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                                                        os_host=os_host,
467                                                        os_username=os_username,
468                                                        os_password=os_password)
469            if rc == 0:
470                os_login = 1
471                os_run_cmd = 1
472            else:
473                gp.dprint_vars(output, stderr)
474                gp.dprint_vars(rc, 1)
475
476    os_state = DotDict()
477    for sub_state in req_states:
478        cmd_buf = "os_state['" + sub_state + "'] = str(" + sub_state + ")"
479        exec(cmd_buf)
480
481    return os_state
482
483
484def get_state(openbmc_host="",
485              openbmc_username="",
486              openbmc_password="",
487              os_host="",
488              os_username="",
489              os_password="",
490              req_states=default_req_states,
491              quiet=None):
492    r"""
493    Get component states such as chassis state, bmc state, etc, put them into a
494    dictionary and return them to the caller.
495
496    Note that all substate values are strings.
497
498    Note: If elapsed_boot_time is included in req_states, it is the caller's
499    duty to call set_start_boot_seconds() in order to set global
500    start_boot_seconds.  elapsed_boot_time is the current time minus
501    start_boot_seconds.
502
503    Description of argument(s):
504    openbmc_host      The DNS name or IP address of the BMC.
505                      This defaults to global ${OPENBMC_HOST}.
506    openbmc_username  The username to be used to login to the BMC.
507                      This defaults to global ${OPENBMC_USERNAME}.
508    openbmc_password  The password to be used to login to the BMC.
509                      This defaults to global ${OPENBMC_PASSWORD}.
510    os_host           The DNS name or IP address of the operating system.
511                      This defaults to global ${OS_HOST}.
512    os_username       The username to be used to login to the OS.
513                      This defaults to global ${OS_USERNAME}.
514    os_password       The password to be used to login to the OS.
515                      This defaults to global ${OS_PASSWORD}.
516    req_states        This is a list of states whose values are being requested
517                      by the caller.
518    quiet             Indicates whether status details (e.g. curl commands)
519                      should be written to the console.
520                      Defaults to either global value of ${QUIET} or to 1.
521    """
522
523    quiet = int(gp.get_var_value(quiet, 0))
524
525    # Set parm defaults where necessary and validate all parms.
526    if openbmc_host == "":
527        openbmc_host = BuiltIn().get_variable_value("${OPENBMC_HOST}")
528    error_message = gv.valid_value(openbmc_host, invalid_values=[None, ""])
529    if error_message != "":
530        BuiltIn().fail(gp.sprint_error(error_message))
531
532    if openbmc_username == "":
533        openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
534    error_message = gv.valid_value(openbmc_username, invalid_values=[None, ""])
535    if error_message != "":
536        BuiltIn().fail(gp.sprint_error(error_message))
537
538    if openbmc_password == "":
539        openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
540    error_message = gv.valid_value(openbmc_password, invalid_values=[None, ""])
541    if error_message != "":
542        BuiltIn().fail(gp.sprint_error(error_message))
543
544    # NOTE: OS parms are optional.
545    if os_host == "":
546        os_host = BuiltIn().get_variable_value("${OS_HOST}")
547        if os_host is None:
548            os_host = ""
549
550    if os_username is "":
551        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
552        if os_username is None:
553            os_username = ""
554
555    if os_password is "":
556        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
557        if os_password is None:
558            os_password = ""
559
560    invalid_req_states = [sub_state for sub_state in req_states
561                          if sub_state not in valid_req_states]
562    if len(invalid_req_states) > 0:
563        error_message = "The following req_states are not supported:\n" +\
564            gp.sprint_var(invalid_req_states)
565        BuiltIn().fail(gp.sprint_error(error_message))
566
567    # Initialize all substate values supported by this function.
568    ping = 0
569    packet_loss = ''
570    uptime = ''
571    epoch_seconds = ''
572    elapsed_boot_time = ''
573    rest = ''
574    redfish = ''
575    chassis = ''
576    requested_chassis = ''
577    bmc = ''
578    requested_bmc = ''
579    # BootProgress state will get populated when state logic enumerates the
580    # state URI. This is to prevent state dictionary  boot_progress value
581    # getting empty when the BootProgress is NOT found, making it optional.
582    boot_progress = 'NA'
583    operating_system = ''
584    host = ''
585    requested_host = ''
586    attempts_left = ''
587
588    # Get the component states.
589    if 'ping' in req_states:
590        # See if the OS pings.
591        rc, out_buf = gc.shell_cmd("ping -c 1 -w 2 " + openbmc_host,
592                                   print_output=0, show_err=0,
593                                   ignore_err=1)
594        if rc == 0:
595            ping = 1
596
597    if 'packet_loss' in req_states:
598        # See if the OS pings.
599        cmd_buf = "ping -c 5 -w 5 " + openbmc_host +\
600            " | egrep 'packet loss' | sed -re 's/.* ([0-9]+)%.*/\\1/g'"
601        rc, out_buf = gc.shell_cmd(cmd_buf,
602                                   print_output=0, show_err=0,
603                                   ignore_err=1)
604        if rc == 0:
605            packet_loss = out_buf.rstrip("\n")
606
607    if 'uptime' in req_states:
608        # Sometimes reading uptime results in a blank value. Call with
609        # wait_until_keyword_succeeds to ensure a non-blank value is obtained.
610        remote_cmd_buf = "read uptime filler 2>/dev/null < /proc/uptime" +\
611            " && [ ! -z \"${uptime}\" ] && echo ${uptime}"
612        cmd_buf = ["BMC Execute Command",
613                   re.sub('\\$', '\\$', remote_cmd_buf), 'quiet=1',
614                   'test_mode=0']
615        gp.qprint_issuing(cmd_buf, 0)
616        gp.qprint_issuing(remote_cmd_buf, 0)
617        try:
618            stdout, stderr, rc =\
619                BuiltIn().wait_until_keyword_succeeds("10 sec", "0 sec",
620                                                      *cmd_buf)
621            if rc == 0 and stderr == "":
622                uptime = stdout
623        except AssertionError as my_assertion_error:
624            pass
625
626    if 'epoch_seconds' in req_states or 'elapsed_boot_time' in req_states:
627        date_cmd_buf = "date -u +%s"
628        if USE_BMC_EPOCH_TIME:
629            cmd_buf = ["BMC Execute Command", date_cmd_buf, 'quiet=${1}']
630            if not quiet:
631                gp.print_issuing(cmd_buf)
632            status, ret_values = \
633                BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
634            if status == "PASS":
635                stdout, stderr, rc = ret_values
636                if rc == 0 and stderr == "":
637                    epoch_seconds = stdout.rstrip("\n")
638        else:
639            shell_rc, out_buf = gc.cmd_fnc_u(date_cmd_buf,
640                                             quiet=quiet,
641                                             print_output=0)
642            if shell_rc == 0:
643                epoch_seconds = out_buf.rstrip("\n")
644
645    if 'elapsed_boot_time' in req_states:
646        global start_boot_seconds
647        elapsed_boot_time = int(epoch_seconds) - start_boot_seconds
648
649    if not redfish_support_trans_state:
650        master_req_rest = ['rest', 'host', 'requested_host', 'operating_system',
651                           'attempts_left', 'boot_progress', 'chassis',
652                           'requested_chassis' 'bmc' 'requested_bmc']
653
654        req_rest = [sub_state for sub_state in req_states if sub_state in
655                    master_req_rest]
656        need_rest = (len(req_rest) > 0)
657        state = DotDict()
658        if need_rest:
659            cmd_buf = ["Read Properties", SYSTEM_STATE_URI + "enumerate",
660                       "quiet=${" + str(quiet) + "}", "timeout=30"]
661            gp.dprint_issuing(cmd_buf)
662            status, ret_values = \
663                BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
664            if status == "PASS":
665                state['rest'] = '1'
666            else:
667                state['rest'] = '0'
668
669            if int(state['rest']):
670                for url_path in ret_values:
671                    # Skip conflicting "CurrentHostState" URL from the enum
672                    # /xyz/openbmc_project/state/hypervisor0
673                    if "hypervisor0" in url_path:
674                        continue
675
676                    for attr_name in ret_values[url_path]:
677                        # Create a state key value based on the attr_name.
678                        try:
679                            ret_values[url_path][attr_name] = \
680                                re.sub(r'.*\.', "",
681                                       ret_values[url_path][attr_name])
682                        except TypeError:
683                            pass
684                        # Do some key name manipulations.
685                        new_attr_name = re.sub(r'^Current|(State|Transition)$',
686                                               "", attr_name)
687                        new_attr_name = re.sub(r'BMC', r'Bmc', new_attr_name)
688                        new_attr_name = re.sub(r'([A-Z][a-z])', r'_\1',
689                                               new_attr_name)
690                        new_attr_name = new_attr_name.lower().lstrip("_")
691                        new_attr_name = re.sub(r'power', r'chassis', new_attr_name)
692                        if new_attr_name in req_states:
693                            state[new_attr_name] = ret_values[url_path][attr_name]
694    else:
695        master_req_rf = ['redfish', 'host', 'requested_host',
696                         'attempts_left', 'boot_progress', 'chassis',
697                         'requested_chassis' 'bmc' 'requested_bmc']
698
699        req_rf = [sub_state for sub_state in req_states if sub_state in
700                  master_req_rf]
701        need_rf = (len(req_rf) > 0)
702        state = DotDict()
703        if need_rf:
704            cmd_buf = ["Redfish Get States"]
705            gp.dprint_issuing(cmd_buf)
706            status, ret_values = \
707                BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
708            gp.dprint_vars(status, ret_values)
709            if status == "PASS":
710                state['redfish'] = '1'
711            else:
712                state['redfish'] = '0'
713
714            if int(state['redfish']):
715                state['chassis'] = ret_values['chassis']
716                state['boot_progress'] = ret_values['boot_progress']
717                state['host'] = ret_values['host']
718                state['bmc'] = ret_values['bmc']
719
720    for sub_state in req_states:
721        if sub_state in state:
722            continue
723        if sub_state.startswith("os_"):
724            # We pass "os_" requests on to get_os_state.
725            continue
726        cmd_buf = "state['" + sub_state + "'] = str(" + sub_state + ")"
727        exec(cmd_buf)
728
729    if os_host == "":
730        # The caller has not specified an os_host so as far as we're concerned,
731        # it doesn't exist.
732        return state
733
734    os_req_states = [sub_state for sub_state in req_states
735                     if sub_state.startswith('os_')]
736
737    if len(os_req_states) > 0:
738        # The caller has specified an os_host and they have requested
739        # information on os substates.
740
741        # Based on the information gathered on bmc, we'll try to make a
742        # determination of whether the os is even up.  We'll pass the result
743        # of that assessment to get_os_state to enhance performance.
744        os_up_match = DotDict()
745        for sub_state in master_os_up_match:
746            if sub_state in req_states:
747                os_up_match[sub_state] = master_os_up_match[sub_state]
748        os_up = compare_states(state, os_up_match)
749        os_state = get_os_state(os_host=os_host,
750                                os_username=os_username,
751                                os_password=os_password,
752                                req_states=os_req_states,
753                                os_up=os_up,
754                                quiet=quiet)
755        # Append os_state dictionary to ours.
756        state.update(os_state)
757
758    return state
759
760
761exit_wait_early_message = ""
762
763
764def set_exit_wait_early_message(value):
765    r"""
766    Set global exit_wait_early_message to the indicated value.
767
768    This is a mechanism by which the programmer can do an early exit from
769    wait_until_keyword_succeeds() based on some special condition.
770
771    Description of argument(s):
772    value                           The value to assign to the global
773                                    exit_wait_early_message.
774    """
775
776    global exit_wait_early_message
777    exit_wait_early_message = value
778
779
780def check_state(match_state,
781                invert=0,
782                print_string="",
783                openbmc_host="",
784                openbmc_username="",
785                openbmc_password="",
786                os_host="",
787                os_username="",
788                os_password="",
789                quiet=None):
790    r"""
791    Check that the Open BMC machine's composite state matches the specified
792    state.  On success, this keyword returns the machine's composite state as a
793    dictionary.
794
795    Description of argument(s):
796    match_state       A dictionary whose key/value pairs are "state field"/
797                      "state value".  The state value is interpreted as a
798                      regular expression.  Example call from robot:
799                      ${match_state}=  Create Dictionary  chassis=^On$
800                      ...  bmc=^Ready$
801                      ...  boot_progress=^OSStart$
802                      ${state}=  Check State  &{match_state}
803    invert            If this flag is set, this function will succeed if the
804                      states do NOT match.
805    print_string      This function will print this string to the console prior
806                      to getting the state.
807    openbmc_host      The DNS name or IP address of the BMC.
808                      This defaults to global ${OPENBMC_HOST}.
809    openbmc_username  The username to be used to login to the BMC.
810                      This defaults to global ${OPENBMC_USERNAME}.
811    openbmc_password  The password to be used to login to the BMC.
812                      This defaults to global ${OPENBMC_PASSWORD}.
813    os_host           The DNS name or IP address of the operating system.
814                      This defaults to global ${OS_HOST}.
815    os_username       The username to be used to login to the OS.
816                      This defaults to global ${OS_USERNAME}.
817    os_password       The password to be used to login to the OS.
818                      This defaults to global ${OS_PASSWORD}.
819    quiet             Indicates whether status details should be written to the
820                      console.  Defaults to either global value of ${QUIET} or
821                      to 1.
822    """
823
824    quiet = int(gp.get_var_value(quiet, 0))
825
826    gp.gp_print(print_string)
827
828    try:
829        match_state = return_state_constant(match_state)
830    except TypeError:
831        pass
832
833    req_states = list(match_state.keys())
834    # Remove special-case match key from req_states.
835    if expressions_key() in req_states:
836        req_states.remove(expressions_key())
837    # Initialize state.
838    state = get_state(openbmc_host=openbmc_host,
839                      openbmc_username=openbmc_username,
840                      openbmc_password=openbmc_password,
841                      os_host=os_host,
842                      os_username=os_username,
843                      os_password=os_password,
844                      req_states=req_states,
845                      quiet=quiet)
846    if not quiet:
847        gp.print_var(state)
848
849    if exit_wait_early_message != "":
850        # The exit_wait_early_message has been set by a signal handler so we
851        # will exit "successfully".  It is incumbent upon the calling function
852        # (e.g. wait_state) to check/clear this variable and to fail
853        # appropriately.
854        return state
855
856    match = compare_states(state, match_state)
857
858    if invert and match:
859        fail_msg = "The current state of the machine matches the match" +\
860                   " state:\n" + gp.sprint_varx("state", state)
861        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
862    elif not invert and not match:
863        fail_msg = "The current state of the machine does NOT match the" +\
864                   " match state:\n" +\
865                   gp.sprint_varx("state", state)
866        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
867
868    return state
869
870
871def wait_state(match_state=(),
872               wait_time="1 min",
873               interval="1 second",
874               invert=0,
875               openbmc_host="",
876               openbmc_username="",
877               openbmc_password="",
878               os_host="",
879               os_username="",
880               os_password="",
881               quiet=None):
882    r"""
883    Wait for the Open BMC machine's composite state to match the specified
884    state.  On success, this keyword returns the machine's composite state as
885    a dictionary.
886
887    Description of argument(s):
888    match_state       A dictionary whose key/value pairs are "state field"/
889                      "state value".  See check_state (above) for details.
890                      This value may also be any string accepted by
891                      return_state_constant (e.g. "standby_match_state").
892                      In such a case this function will call
893                      return_state_constant to convert it to a proper
894                      dictionary as described above.
895    wait_time         The total amount of time to wait for the desired state.
896                      This value may be expressed in Robot Framework's time
897                      format (e.g. 1 minute, 2 min 3 s, 4.5).
898    interval          The amount of time between state checks.
899                      This value may be expressed in Robot Framework's time
900                      format (e.g. 1 minute, 2 min 3 s, 4.5).
901    invert            If this flag is set, this function will for the state of
902                      the machine to cease to match the match state.
903    openbmc_host      The DNS name or IP address of the BMC.
904                      This defaults to global ${OPENBMC_HOST}.
905    openbmc_username  The username to be used to login to the BMC.
906                      This defaults to global ${OPENBMC_USERNAME}.
907    openbmc_password  The password to be used to login to the BMC.
908                      This defaults to global ${OPENBMC_PASSWORD}.
909    os_host           The DNS name or IP address of the operating system.
910                      This defaults to global ${OS_HOST}.
911    os_username       The username to be used to login to the OS.
912                      This defaults to global ${OS_USERNAME}.
913    os_password       The password to be used to login to the OS.
914                      This defaults to global ${OS_PASSWORD}.
915    quiet             Indicates whether status details should be written to the
916                      console.  Defaults to either global value of ${QUIET} or
917                      to 1.
918    """
919
920    quiet = int(gp.get_var_value(quiet, 0))
921
922    try:
923        match_state = return_state_constant(match_state)
924    except TypeError:
925        pass
926
927    if not quiet:
928        if invert:
929            alt_text = "cease to "
930        else:
931            alt_text = ""
932        gp.print_timen("Checking every " + str(interval) + " for up to "
933                       + str(wait_time) + " for the state of the machine to "
934                       + alt_text + "match the state shown below.")
935        gp.print_var(match_state)
936
937    if quiet:
938        print_string = ""
939    else:
940        print_string = "#"
941
942    debug = int(BuiltIn().get_variable_value("${debug}", "0"))
943    if debug:
944        # In debug we print state so no need to print the "#".
945        print_string = ""
946    check_state_quiet = 1 - debug
947    cmd_buf = ["Check State", match_state, "invert=${" + str(invert) + "}",
948               "print_string=" + print_string, "openbmc_host=" + openbmc_host,
949               "openbmc_username=" + openbmc_username,
950               "openbmc_password=" + openbmc_password, "os_host=" + os_host,
951               "os_username=" + os_username, "os_password=" + os_password,
952               "quiet=${" + str(check_state_quiet) + "}"]
953    gp.dprint_issuing(cmd_buf)
954    try:
955        state = BuiltIn().wait_until_keyword_succeeds(wait_time, interval,
956                                                      *cmd_buf)
957    except AssertionError as my_assertion_error:
958        gp.printn()
959        message = my_assertion_error.args[0]
960        BuiltIn().fail(message)
961
962    if exit_wait_early_message:
963        # The global exit_wait_early_message was set by a signal handler
964        # indicating that we should fail.
965        message = exit_wait_early_message
966        # Clear the exit_wait_early_message variable for future use.
967        set_exit_wait_early_message("")
968        BuiltIn().fail(gp.sprint_error(message))
969
970    if not quiet:
971        gp.printn()
972        if invert:
973            gp.print_timen("The states no longer match:")
974        else:
975            gp.print_timen("The states match:")
976        gp.print_var(state)
977
978    return state
979
980
981def set_start_boot_seconds(value=0):
982    global start_boot_seconds
983    start_boot_seconds = int(value)
984
985
986set_start_boot_seconds(0)
987
988
989def wait_for_comm_cycle(start_boot_seconds,
990                        quiet=None):
991    r"""
992    Wait for the BMC uptime to be less than elapsed_boot_time.
993
994    This function will tolerate an expected loss of communication to the BMC.
995    This function is useful when some kind of reboot has been initiated by the
996    caller.
997
998    Description of argument(s):
999    start_boot_seconds  The time that the boot test started.  The format is the
1000                        epoch time in seconds, i.e. the number of seconds since
1001                        1970-01-01 00:00:00 UTC.  This value should be obtained
1002                        from the BMC so that it is not dependent on any kind of
1003                        synchronization between this machine and the target BMC
1004                        This will allow this program to work correctly even in
1005                        a simulated environment.  This value should be obtained
1006                        by the caller prior to initiating a reboot.  It can be
1007                        obtained as follows:
1008                        state = st.get_state(req_states=['epoch_seconds'])
1009    """
1010
1011    quiet = int(gp.get_var_value(quiet, 0))
1012
1013    # Validate parms.
1014    error_message = gv.valid_integer(start_boot_seconds)
1015    if error_message:
1016        BuiltIn().fail(gp.sprint_error(error_message))
1017
1018    # Wait for uptime to be less than elapsed_boot_time.
1019    set_start_boot_seconds(start_boot_seconds)
1020    expr = 'int(float(state[\'uptime\'])) < int(state[\'elapsed_boot_time\'])'
1021    match_state = DotDict([('uptime', '^[0-9\\.]+$'),
1022                           ('elapsed_boot_time', '^[0-9]+$'),
1023                           (expressions_key(), [expr])])
1024    wait_state(match_state, wait_time="12 mins", interval="5 seconds")
1025
1026    gp.qprint_timen("Verifying that REST/Redfish API interface is working.")
1027    if not redfish_support_trans_state:
1028        match_state = DotDict([('rest', '^1$')])
1029    else:
1030        match_state = DotDict([('redfish', '^1$')])
1031    state = wait_state(match_state, wait_time="5 mins", interval="2 seconds")
1032