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