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