pytools: obmcutil: Implement blocking behaviour

The technique to block is to attach a listener for systemd JobRemoved
property change events, change the desired OpenBMC state
management property to trigger the systemd transition, and then run the
gobject dbus mainloop. We terminate the mainloop by invoking quit() in
the callback on the captured mainloop object.

Additionally, the result of the transition (success or failure) is
judged in the callback. We can't obtain the result by returning it, and
as we are using Python 2 we cannot meaningfully mutate captured
variables to expose it. Instead, a variable is created on the callback
function object after it is defined but before the function is invoked,
which is mutated inside the function (which can reference its own
object) when called. The result is retrieved after the mainloop has
terminated and is propagated up the call chain to form the exit status.

Change-Id: Ic19aa604631177abea7580de2357d8c6812ee874
Signed-off-by: Andrew Jeffery <andrew@aj.id.au>
diff --git a/pytools/obmcutil b/pytools/obmcutil
index d0db8b5..aee294b 100644
--- a/pytools/obmcutil
+++ b/pytools/obmcutil
@@ -4,6 +4,9 @@
 import dbus
 import argparse
 
+from dbus.mainloop.glib import DBusGMainLoop
+import gobject
+
 descriptors = {
     'power': {
         'bus_name': 'org.openbmc.control.Power',
@@ -15,28 +18,32 @@
         'object_name': '/xyz/openbmc_project/state/chassis0',
         'interface_name': 'xyz.openbmc_project.State.Chassis',
         'property': 'RequestedPowerTransition',
-        'value': 'xyz.openbmc_project.State.Chassis.Transition.On'
+        'value': 'xyz.openbmc_project.State.Chassis.Transition.On',
+        'monitor': 'obmc-chassis-poweron@0.target',
     },
     'chassisoff': {
         'bus_name': 'xyz.openbmc_project.State.Chassis',
         'object_name': '/xyz/openbmc_project/state/chassis0',
         'interface_name': 'xyz.openbmc_project.State.Chassis',
         'property': 'RequestedPowerTransition',
-        'value': 'xyz.openbmc_project.State.Chassis.Transition.Off'
+        'value': 'xyz.openbmc_project.State.Chassis.Transition.Off',
+        'monitor': 'obmc-chassis-hard-poweroff@0.target',
     },
     'poweron': {
         'bus_name': 'xyz.openbmc_project.State.Host',
         'object_name': '/xyz/openbmc_project/state/host0',
         'interface_name': 'xyz.openbmc_project.State.Host',
         'property': 'RequestedHostTransition',
-        'value': 'xyz.openbmc_project.State.Host.Transition.On'
+        'value': 'xyz.openbmc_project.State.Host.Transition.On',
+        'monitor': 'obmc-host-start@0.target',
     },
     'poweroff': {
         'bus_name': 'xyz.openbmc_project.State.Host',
         'object_name': '/xyz/openbmc_project/state/host0',
         'interface_name': 'xyz.openbmc_project.State.Host',
         'property': 'RequestedHostTransition',
-        'value': 'xyz.openbmc_project.State.Host.Transition.Off'
+        'value': 'xyz.openbmc_project.State.Host.Transition.Off',
+        'monitor': 'obmc-host-stop@0.target',
     },
     'bmcstate': {
         'bus_name': 'xyz.openbmc_project.State.BMC',
@@ -65,45 +72,88 @@
     'state' : ['bmcstate', 'chassisstate', 'hoststate']
 }
 
-def run_one_command(dbus_bus, descriptor):
+def run_set_property(dbus_bus, dbus_iface, descriptor, args):
+    mainloop = gobject.MainLoop()
+
+    iface = descriptor['interface_name']
+    prop = descriptor['property']
+
+    if 'monitor' not in descriptor:
+        dbus_iface.Set(iface, prop, descriptor['value'])
+        return True
+
+    def property_listener(job, path, unit, state):
+        if descriptor['monitor'] != unit:
+            return
+
+        property_listener.success = (state == 'done')
+        mainloop.quit()
+
+    property_listener.success = True
+
+    if args.wait:
+        sig_match = dbus_bus.add_signal_receiver(property_listener, "JobRemoved")
+
+    dbus_iface.Set(iface, prop, descriptor['value'])
+
+    if args.wait:
+        mainloop.run()
+        sig_match.remove()
+
+    return property_listener.success
+
+def run_one_command(dbus_bus, descriptor, args):
     bus = descriptor['bus_name']
     obj = descriptor['object_name']
     iface = descriptor['interface_name']
     dbus_obj = dbus_bus.get_object(bus, obj)
+    result = None
 
     if (descriptor.has_key('property')):
-        prop = descriptor['property']
         dbus_iface = dbus.Interface(dbus_obj, "org.freedesktop.DBus.Properties")
         if descriptor.has_key('value'):
-            dbus_iface.Set(iface, prop, descriptor['value'])
+            result = run_set_property(dbus_bus, dbus_iface, descriptor, args)
         else:
+            prop = descriptor['property']
             dbus_prop = dbus_iface.Get(iface, prop)
             print '{:<20}: {}'.format(prop, str(dbus_prop))
+            result = True
     else:
         dbus_iface = dbus.Interface(dbus_obj, "org.freedesktop.DBus.Properties")
         props = dbus_iface.GetAll(iface)
         for p in props:
             print "{} = {}".format(p, str(props[p]))
+        result = True
 
-def run_all_commands(dbus_bus, recipe):
+    return result
+
+def run_all_commands(dbus_bus, recipe, args):
     if isinstance(recipe, dict):
-        run_one_command(dbus_bus, recipe)
-        return
+        return run_one_command(dbus_bus, recipe, args)
 
     assert isinstance(recipe, list)
     for command in recipe:
-        run_one_command(dbus_bus, descriptors[command])
+        descriptor = descriptors[command]
+        if not run_one_command(dbus_bus, descriptor, args):
+            print "Failed to execute command: {}".format(descriptor)
+            return False
+
+    return True
 
 def main():
+    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
     parser = argparse.ArgumentParser()
+    parser.add_argument('--wait', '-w', action='store_true',
+            help='Block until the state transition succeeds or fails')
     parser.add_argument('recipe', choices=sorted(descriptors.keys()))
     args = parser.parse_args()
 
     dbus_bus = dbus.SystemBus()
     try:
-        run_all_commands(dbus_bus, descriptors[args.recipe], args)
+        return run_all_commands(dbus_bus, descriptors[args.recipe], args)
     finally:
         dbus_bus.close()
 
 if __name__ == "__main__":
-    main()
+    sys.exit(0 if main() else 1)