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