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
11state:
12  state[power]:                                   1
13  state[bmc]:                                     HOST_BOOTED
14  state[boot_progress]:                           FW Progress, Starting OS
15  state[os_ping]:                                 1
16  state[os_login]:                                1
17  state[os_run_cmd]:                              1
18
19Different users may very well have different needs when inquiring about
20state.  In the future, we can add code to allow a user to specify which
21pieces of info they need in the state dictionary.  Examples of such data
22might include uptime, state timestamps, boot side, etc.
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
33
34import commands
35from robot.libraries.BuiltIn import BuiltIn
36
37import re
38
39# We need utils.robot to get keywords like "Get Power State".
40BuiltIn().import_resource("utils.robot")
41
42
43###############################################################################
44def anchor_state(state):
45
46    r"""
47    Add regular expression anchors ("^" and "$") to the beginning and end of
48    each item in the state dictionary passed in.  Return the resulting
49    dictionary.
50
51    Description of Arguments:
52    state    A dictionary such as the one returned by the get_state()
53             function.
54    """
55
56    anchored_state = state
57    for key, match_state_value in anchored_state.items():
58        anchored_state[key] = "^" + str(anchored_state[key]) + "$"
59
60    return anchored_state
61
62###############################################################################
63
64
65###############################################################################
66def compare_states(state,
67                   match_state):
68
69    r"""
70    Compare 2 state dictionaries.  Return True if the match and False if they
71    don't.  Note that the match_state dictionary does not need to have an entry
72    corresponding to each entry in the state dictionary.  But for each entry
73    that it does have, the corresponding state entry will be checked for a
74    match.
75
76    Description of arguments:
77    state           A state dictionary such as the one returned by the
78                    get_state function.
79    match_state     A dictionary whose key/value pairs are "state field"/
80                    "state value".  The state value is interpreted as a
81                    regular expression.  Every value in this dictionary is
82                    considered.  If each and every one matches, the 2
83                    dictionaries are considered to be matching.
84    """
85
86    match = True
87    for key, match_state_value in match_state.items():
88        try:
89            if not re.match(match_state_value, str(state[key])):
90                match = False
91                break
92        except KeyError:
93            match = False
94            break
95
96    return match
97
98###############################################################################
99
100
101###############################################################################
102def get_os_state(os_host="",
103                 os_username="",
104                 os_password="",
105                 quiet=None):
106
107    r"""
108    Get component states for the operating system such as ping, login,
109    etc, put them into a dictionary and return them to the caller.
110
111    Description of arguments:
112    os_host      The DNS name or IP address of the operating system.
113                 This defaults to global ${OS_HOST}.
114    os_username  The username to be used to login to the OS.
115                 This defaults to global ${OS_USERNAME}.
116    os_password  The password to be used to login to the OS.
117                 This defaults to global ${OS_PASSWORD}.
118    quiet        Indicates whether status details (e.g. curl commands) should
119                 be written to the console.
120                 Defaults to either global value of ${QUIET} or to 1.
121    """
122
123    quiet = grp.set_quiet_default(quiet, 1)
124
125    # Set parm defaults where necessary and validate all parms.
126    if os_host == "":
127        os_host = BuiltIn().get_variable_value("${OS_HOST}")
128    error_message = gv.svalid_value(os_host, var_name="os_host",
129                                    invalid_values=[None, ""])
130    if error_message != "":
131        BuiltIn().fail(gp.sprint_error(error_message))
132
133    if os_username == "":
134        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
135    error_message = gv.svalid_value(os_username, var_name="os_username",
136                                    invalid_values=[None, ""])
137    if error_message != "":
138        BuiltIn().fail(gp.sprint_error(error_message))
139
140    if os_password == "":
141        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
142    error_message = gv.svalid_value(os_password, var_name="os_password",
143                                    invalid_values=[None, ""])
144    if error_message != "":
145        BuiltIn().fail(gp.sprint_error(error_message))
146
147    # See if the OS pings.
148    cmd_buf = "ping -c 1 -w 2 " + os_host
149    if not quiet:
150        grp.rpissuing(cmd_buf)
151    rc, out_buf = commands.getstatusoutput(cmd_buf)
152    if rc == 0:
153        pings = 1
154    else:
155        pings = 0
156
157    # Open SSH connection to OS.
158    cmd_buf = ["Open Connection", os_host]
159    if not quiet:
160        grp.rpissuing_keyword(cmd_buf)
161    ix = BuiltIn().run_keyword(*cmd_buf)
162
163    # Login to OS.
164    cmd_buf = ["Login", os_username, os_password]
165    if not quiet:
166        grp.rpissuing_keyword(cmd_buf)
167    status, msg = BuiltIn().run_keyword_and_ignore_error(*cmd_buf)
168
169    if status == "PASS":
170        login = 1
171    else:
172        login = 0
173
174    if login:
175        # Try running a simple command (uptime) on the OS.
176        cmd_buf = ["Execute Command", "uptime", "return_stderr=True",
177                   "return_rc=True"]
178        if not quiet:
179            grp.rpissuing_keyword(cmd_buf)
180        output, stderr_buf, rc = BuiltIn().run_keyword(*cmd_buf)
181        if rc == 0 and stderr_buf == "":
182            run_cmd = 1
183        else:
184            run_cmd = 0
185    else:
186        run_cmd = 0
187
188    # Create a dictionary containing the results of the prior commands.
189    cmd_buf = ["Create Dictionary", "ping=${" + str(pings) + "}",
190               "login=${" + str(login) + "}",
191               "run_cmd=${" + str(run_cmd) + "}"]
192    grp.rdpissuing_keyword(cmd_buf)
193    os_state = BuiltIn().run_keyword(*cmd_buf)
194
195    return os_state
196
197###############################################################################
198
199
200###############################################################################
201def get_state(openbmc_host="",
202              openbmc_username="",
203              openbmc_password="",
204              os_host="",
205              os_username="",
206              os_password="",
207              quiet=None):
208
209    r"""
210    Get component states such as power state, bmc state, etc, put them into a
211    dictionary and return them to the caller.
212
213    Description of arguments:
214    openbmc_host      The DNS name or IP address of the BMC.
215                      This defaults to global ${OPENBMC_HOST}.
216    openbmc_username  The username to be used to login to the BMC.
217                      This defaults to global ${OPENBMC_USERNAME}.
218    openbmc_password  The password to be used to login to the BMC.
219                      This defaults to global ${OPENBMC_PASSWORD}.
220    os_host           The DNS name or IP address of the operating system.
221                      This defaults to global ${OS_HOST}.
222    os_username       The username to be used to login to the OS.
223                      This defaults to global ${OS_USERNAME}.
224    os_password       The password to be used to login to the OS.
225                      This defaults to global ${OS_PASSWORD}.
226    quiet             Indicates whether status details (e.g. curl commands)
227                      should be written to the console.
228                      Defaults to either global value of ${QUIET} or to 1.
229    """
230
231    quiet = grp.set_quiet_default(quiet, 1)
232
233    # Set parm defaults where necessary and validate all parms.
234    if openbmc_host == "":
235        openbmc_host = BuiltIn().get_variable_value("${OPENBMC_HOST}")
236    error_message = gv.svalid_value(openbmc_host,
237                                    var_name="openbmc_host",
238                                    invalid_values=[None, ""])
239    if error_message != "":
240        BuiltIn().fail(gp.sprint_error(error_message))
241
242    if openbmc_username == "":
243        openbmc_username = BuiltIn().get_variable_value("${OPENBMC_USERNAME}")
244    error_message = gv.svalid_value(openbmc_username,
245                                    var_name="openbmc_username",
246                                    invalid_values=[None, ""])
247    if error_message != "":
248        BuiltIn().fail(gp.sprint_error(error_message))
249
250    if openbmc_password == "":
251        openbmc_password = BuiltIn().get_variable_value("${OPENBMC_PASSWORD}")
252    error_message = gv.svalid_value(openbmc_password,
253                                    var_name="openbmc_password",
254                                    invalid_values=[None, ""])
255    if error_message != "":
256        BuiltIn().fail(gp.sprint_error(error_message))
257
258    # Set parm defaults where necessary and validate all parms.  NOTE: OS parms
259    # are optional.
260    if os_host == "":
261        os_host = BuiltIn().get_variable_value("${OS_HOST}")
262        if os_host is None:
263            os_host = ""
264
265    if os_username is "":
266        os_username = BuiltIn().get_variable_value("${OS_USERNAME}")
267        if os_username is None:
268            os_username = ""
269
270    if os_password is "":
271        os_password = BuiltIn().get_variable_value("${OS_PASSWORD}")
272        if os_password is None:
273            os_password = ""
274
275    # Get the component states.
276    cmd_buf = ["Get Power State", "quiet=${" + str(quiet) + "}"]
277    grp.rdpissuing_keyword(cmd_buf)
278    power = BuiltIn().run_keyword(*cmd_buf)
279
280    cmd_buf = ["Get BMC State", "quiet=${" + str(quiet) + "}"]
281    grp.rdpissuing_keyword(cmd_buf)
282    bmc = BuiltIn().run_keyword(*cmd_buf)
283
284    cmd_buf = ["Get Boot Progress", "quiet=${" + str(quiet) + "}"]
285    grp.rdpissuing_keyword(cmd_buf)
286    boot_progress = BuiltIn().run_keyword(*cmd_buf)
287
288    # Create composite state dictionary.
289    cmd_buf = ["Create Dictionary", "power=${" + str(power) + "}",
290               "bmc=" + bmc, "boot_progress=" + boot_progress]
291    grp.rdpissuing_keyword(cmd_buf)
292    state = BuiltIn().run_keyword(*cmd_buf)
293
294    if os_host != "":
295        # Create an os_up_match dictionary to test whether we are booted enough
296        # to get operating system info.
297        cmd_buf = ["Create Dictionary", "power=^${1}$", "bmc=^HOST_BOOTED$",
298                   "boot_progress=^FW Progress, Starting OS$"]
299        grp.rdpissuing_keyword(cmd_buf)
300        os_up_match = BuiltIn().run_keyword(*cmd_buf)
301        os_up = compare_states(state, os_up_match)
302
303        if os_up:
304            # Get OS information...
305            os_state = get_os_state(os_host=os_host,
306                                    os_username=os_username,
307                                    os_password=os_password,
308                                    quiet=quiet)
309            for key, state_value in os_state.items():
310                # Add each OS value to the state dictionary, pre-pending
311                # "os_" to each key.
312                new_key = "os_" + key
313                state[new_key] = state_value
314
315    return state
316
317###############################################################################
318
319
320###############################################################################
321def check_state(match_state,
322                invert=0,
323                print_string="",
324                openbmc_host="",
325                openbmc_username="",
326                openbmc_password="",
327                os_host="",
328                os_username="",
329                os_password="",
330                quiet=None):
331
332    r"""
333    Check that the Open BMC machine's composite state matches the specified
334    state.  On success, this keyword returns the machine's composite state as a
335    dictionary.
336
337    Description of arguments:
338    match_state       A dictionary whose key/value pairs are "state field"/
339                      "state value".  The state value is interpreted as a
340                      regular expression.  Example call from robot:
341                      ${match_state}=  Create Dictionary  power=^1$
342                      ...  bmc=^HOST_BOOTED$
343                      ...  boot_progress=^FW Progress, Starting OS$
344                      ${state}=  Check State  &{match_state}
345    invert            If this flag is set, this function will succeed if the
346                      states do NOT match.
347    print_string      This function will print this string to the console prior
348                      to getting the state.
349    openbmc_host      The DNS name or IP address of the BMC.
350                      This defaults to global ${OPENBMC_HOST}.
351    openbmc_username  The username to be used to login to the BMC.
352                      This defaults to global ${OPENBMC_USERNAME}.
353    openbmc_password  The password to be used to login to the BMC.
354                      This defaults to global ${OPENBMC_PASSWORD}.
355    os_host           The DNS name or IP address of the operating system.
356                      This defaults to global ${OS_HOST}.
357    os_username       The username to be used to login to the OS.
358                      This defaults to global ${OS_USERNAME}.
359    os_password       The password to be used to login to the OS.
360                      This defaults to global ${OS_PASSWORD}.
361    quiet             Indicates whether status details should be written to the
362                      console.  Defaults to either global value of ${QUIET} or
363                      to 1.
364    """
365
366    quiet = grp.set_quiet_default(quiet, 1)
367
368    grp.rprint(print_string)
369
370    # Initialize state.
371    state = get_state(openbmc_host=openbmc_host,
372                      openbmc_username=openbmc_username,
373                      openbmc_password=openbmc_password,
374                      os_host=os_host,
375                      os_username=os_username,
376                      os_password=os_password,
377                      quiet=quiet)
378    if not quiet:
379        grp.rprint_var(state)
380
381    match = compare_states(state, match_state)
382
383    if invert and match:
384        fail_msg = "The current state of the machine matches the match" +\
385                   " state:\n" + gp.sprint_varx("state", state)
386        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
387    elif not invert and not match:
388        fail_msg = "The current state of the machine does NOT match the" +\
389                   " match state:\n" +\
390                   gp.sprint_varx("state", state)
391        BuiltIn().fail("\n" + gp.sprint_error(fail_msg))
392
393    return state
394
395###############################################################################
396
397
398###############################################################################
399def wait_state(match_state,
400               wait_time="1 min",
401               interval="1 second",
402               invert=0,
403               openbmc_host="",
404               openbmc_username="",
405               openbmc_password="",
406               os_host="",
407               os_username="",
408               os_password="",
409               quiet=None):
410
411    r"""
412    Wait for the Open BMC machine's composite state to match the specified
413    state.  On success, this keyword returns the machine's composite state as
414    a dictionary.
415
416    Description of arguments:
417    match_state       A dictionary whose key/value pairs are "state field"/
418                      "state value".  See check_state (above) for details.
419    wait_time         The total amount of time to wait for the desired state.
420                      This value may be expressed in Robot Framework's time
421                      format (e.g. 1 minute, 2 min 3 s, 4.5).
422    interval          The amount of time between state checks.
423                      This value may be expressed in Robot Framework's time
424                      format (e.g. 1 minute, 2 min 3 s, 4.5).
425    invert            If this flag is set, this function will for the state of
426                      the machine to cease to match the match state.
427    openbmc_host      The DNS name or IP address of the BMC.
428                      This defaults to global ${OPENBMC_HOST}.
429    openbmc_username  The username to be used to login to the BMC.
430                      This defaults to global ${OPENBMC_USERNAME}.
431    openbmc_password  The password to be used to login to the BMC.
432                      This defaults to global ${OPENBMC_PASSWORD}.
433    os_host           The DNS name or IP address of the operating system.
434                      This defaults to global ${OS_HOST}.
435    os_username       The username to be used to login to the OS.
436                      This defaults to global ${OS_USERNAME}.
437    os_password       The password to be used to login to the OS.
438                      This defaults to global ${OS_PASSWORD}.
439    quiet             Indicates whether status details should be written to the
440                      console.  Defaults to either global value of ${QUIET} or
441                      to 1.
442    """
443
444    quiet = grp.set_quiet_default(quiet, 1)
445
446    if not quiet:
447        if invert:
448            alt_text = "cease to "
449        else:
450            alt_text = ""
451        grp.rprint_timen("Checking every " + str(interval) + " for up to " +
452                         str(wait_time) + " for the state of the machine to " +
453                         alt_text + "match the state shown below.")
454        grp.rprint_var(match_state)
455
456    cmd_buf = ["Check State", match_state, "invert=${" + str(invert) + "}",
457               "print_string=#", "openbmc_host=" + openbmc_host,
458               "openbmc_username=" + openbmc_username,
459               "openbmc_password=" + openbmc_password, "os_host=" + os_host,
460               "os_username=" + os_username, "os_password=" + os_password,
461               "quiet=${1}"]
462    grp.rdpissuing_keyword(cmd_buf)
463    state = BuiltIn().wait_until_keyword_succeeds(wait_time, interval,
464                                                  *cmd_buf)
465
466    if not quiet:
467        grp.rprintn()
468        if invert:
469            grp.rprint_timen("The states no longer match:")
470        else:
471            grp.rprint_timen("The states match:")
472        grp.rprint_var(state)
473
474    return state
475
476###############################################################################
477