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