Brad Bishop | c0da847 | 2019-04-04 08:29:42 -0400 | [diff] [blame^] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | import sys |
| 4 | import dbus |
| 5 | import argparse |
| 6 | |
| 7 | from dbus.mainloop.glib import DBusGMainLoop |
| 8 | import gobject |
| 9 | import json |
| 10 | import os |
| 11 | import signal |
| 12 | import time |
| 13 | from glob import glob |
| 14 | from os.path import join |
| 15 | from subprocess import Popen |
| 16 | |
| 17 | import obmc_system_config |
| 18 | |
| 19 | descriptors = { |
| 20 | 'power': { |
| 21 | 'bus_name': 'org.openbmc.control.Power', |
| 22 | 'object_name': '/org/openbmc/control/power0', |
| 23 | 'interface_name': 'org.openbmc.control.Power' |
| 24 | }, |
| 25 | 'chassison': { |
| 26 | 'bus_name': 'xyz.openbmc_project.State.Chassis', |
| 27 | 'object_name': '/xyz/openbmc_project/state/chassis0', |
| 28 | 'interface_name': 'xyz.openbmc_project.State.Chassis', |
| 29 | 'property': 'RequestedPowerTransition', |
| 30 | 'value': 'xyz.openbmc_project.State.Chassis.Transition.On', |
| 31 | 'monitor': 'obmc-chassis-poweron@0.target', |
| 32 | }, |
| 33 | 'chassisoff': { |
| 34 | 'bus_name': 'xyz.openbmc_project.State.Chassis', |
| 35 | 'object_name': '/xyz/openbmc_project/state/chassis0', |
| 36 | 'interface_name': 'xyz.openbmc_project.State.Chassis', |
| 37 | 'property': 'RequestedPowerTransition', |
| 38 | 'value': 'xyz.openbmc_project.State.Chassis.Transition.Off', |
| 39 | 'monitor': 'obmc-chassis-hard-poweroff@0.target', |
| 40 | }, |
| 41 | 'poweron': { |
| 42 | 'bus_name': 'xyz.openbmc_project.State.Host', |
| 43 | 'object_name': '/xyz/openbmc_project/state/host0', |
| 44 | 'interface_name': 'xyz.openbmc_project.State.Host', |
| 45 | 'property': 'RequestedHostTransition', |
| 46 | 'value': 'xyz.openbmc_project.State.Host.Transition.On', |
| 47 | 'monitor': 'obmc-host-start@0.target', |
| 48 | }, |
| 49 | 'poweroff': { |
| 50 | 'bus_name': 'xyz.openbmc_project.State.Host', |
| 51 | 'object_name': '/xyz/openbmc_project/state/host0', |
| 52 | 'interface_name': 'xyz.openbmc_project.State.Host', |
| 53 | 'property': 'RequestedHostTransition', |
| 54 | 'value': 'xyz.openbmc_project.State.Host.Transition.Off', |
| 55 | 'monitor': 'obmc-host-stop@0.target', |
| 56 | }, |
| 57 | 'bmcstate': { |
| 58 | 'bus_name': 'xyz.openbmc_project.State.BMC', |
| 59 | 'object_name': '/xyz/openbmc_project/state/bmc0', |
| 60 | 'interface_name': 'xyz.openbmc_project.State.BMC', |
| 61 | 'property': 'CurrentBMCState', |
| 62 | }, |
| 63 | 'chassisstate': { |
| 64 | 'bus_name': 'xyz.openbmc_project.State.Chassis', |
| 65 | 'object_name': '/xyz/openbmc_project/state/chassis0', |
| 66 | 'interface_name': 'xyz.openbmc_project.State.Chassis', |
| 67 | 'property': 'CurrentPowerState', |
| 68 | }, |
| 69 | 'hoststate': { |
| 70 | 'bus_name': 'xyz.openbmc_project.State.Host', |
| 71 | 'object_name': '/xyz/openbmc_project/state/host0', |
| 72 | 'interface_name': 'xyz.openbmc_project.State.Host', |
| 73 | 'property': 'CurrentHostState', |
| 74 | }, |
| 75 | 'bootprogress': { |
| 76 | 'bus_name': 'xyz.openbmc_project.State.Host', |
| 77 | 'object_name': '/xyz/openbmc_project/state/host0', |
| 78 | 'interface_name': 'xyz.openbmc_project.State.Boot.Progress', |
| 79 | 'property': 'BootProgress', |
| 80 | }, |
| 81 | 'state' : ['bmcstate', 'chassisstate', 'hoststate'], |
| 82 | 'status' : ['bmcstate', 'chassisstate', 'hoststate'], |
| 83 | } |
| 84 | |
| 85 | GPIO_DEFS_FILE = '/etc/default/obmc/gpio/gpio_defs.json' |
| 86 | |
| 87 | |
| 88 | def find_gpio_base(path="/sys/class/gpio/"): |
| 89 | pattern = "gpiochip*" |
| 90 | for gc in glob(join(path, pattern)): |
| 91 | with open(join(gc, "label")) as f: |
| 92 | label = f.readline().strip() |
| 93 | if label == "1e780000.gpio": |
| 94 | with open(join(gc, "base")) as f: |
| 95 | return int(f.readline().strip()) |
| 96 | # trigger a file not found exception |
| 97 | open(join(path, "gpiochip")) |
| 98 | |
| 99 | |
| 100 | GPIO_BASE = find_gpio_base() |
| 101 | |
| 102 | |
| 103 | def convertGpio(name): |
| 104 | offset = int(''.join(list(filter(str.isdigit, name)))) |
| 105 | port = list(filter(str.isalpha, name.upper())) |
| 106 | a = ord(port[-1]) - ord('A') |
| 107 | if len(port) > 1: |
| 108 | a += 26 |
| 109 | base = a * 8 + GPIO_BASE |
| 110 | return base + offset |
| 111 | |
| 112 | |
| 113 | def run_set_property(dbus_bus, dbus_iface, descriptor, args): |
| 114 | mainloop = gobject.MainLoop() |
| 115 | |
| 116 | iface = descriptor['interface_name'] |
| 117 | prop = descriptor['property'] |
| 118 | |
| 119 | if 'monitor' not in descriptor: |
| 120 | dbus_iface.Set(iface, prop, descriptor['value']) |
| 121 | return True |
| 122 | |
| 123 | def property_listener(job, path, unit, state): |
| 124 | if descriptor['monitor'] != unit: |
| 125 | return |
| 126 | |
| 127 | property_listener.success = (state == 'done') |
| 128 | mainloop.quit() |
| 129 | |
| 130 | property_listener.success = True |
| 131 | |
| 132 | if args.wait and args.verbose: |
| 133 | pid = Popen(["/bin/journalctl", "-f", "--no-pager"]).pid |
| 134 | |
| 135 | if args.wait: |
| 136 | sig_match = dbus_bus.add_signal_receiver(property_listener, "JobRemoved") |
| 137 | |
| 138 | dbus_iface.Set(iface, prop, descriptor['value']) |
| 139 | |
| 140 | if args.wait: |
| 141 | mainloop.run() |
| 142 | sig_match.remove() |
| 143 | |
| 144 | if args.wait and args.verbose: |
| 145 | # wait some time for the journal output |
| 146 | time.sleep(args.wait_tune) |
| 147 | os.kill(pid, signal.SIGTERM) |
| 148 | |
| 149 | return property_listener.success |
| 150 | |
| 151 | def get_dbus_obj(dbus_bus, bus, obj, args): |
| 152 | if not args.wait: |
| 153 | return dbus_bus.get_object(bus, obj) |
| 154 | |
| 155 | mainloop = gobject.MainLoop() |
| 156 | |
| 157 | def property_listener(job, path, unit, state): |
| 158 | if 'obmc-standby.target' == unit: |
| 159 | mainloop.quit() |
| 160 | |
| 161 | sig_match = dbus_bus.add_signal_receiver(property_listener, "JobRemoved") |
| 162 | try: |
| 163 | return dbus_bus.get_object(bus, obj) |
| 164 | except dbus.exceptions.DBusException as e: |
| 165 | if args.verbose: |
| 166 | pid = Popen(["/bin/journalctl", "-f", "--no-pager"]).pid |
| 167 | |
| 168 | mainloop.run() |
| 169 | |
| 170 | if args.verbose: |
| 171 | os.kill(pid, signal.SIGTERM) |
| 172 | finally: |
| 173 | sig_match.remove() |
| 174 | |
| 175 | return dbus_bus.get_object(bus, obj) |
| 176 | |
| 177 | def run_one_command(dbus_bus, descriptor, args): |
| 178 | bus = descriptor['bus_name'] |
| 179 | obj = descriptor['object_name'] |
| 180 | iface = descriptor['interface_name'] |
| 181 | dbus_obj = get_dbus_obj(dbus_bus, bus, obj, args) |
| 182 | result = None |
| 183 | |
| 184 | if 'property' in descriptor: |
| 185 | dbus_iface = dbus.Interface(dbus_obj, "org.freedesktop.DBus.Properties") |
| 186 | if 'value' in descriptor: |
| 187 | result = run_set_property(dbus_bus, dbus_iface, descriptor, args) |
| 188 | else: |
| 189 | prop = descriptor['property'] |
| 190 | dbus_prop = dbus_iface.Get(iface, prop) |
| 191 | print '{:<20}: {}'.format(prop, str(dbus_prop)) |
| 192 | result = True |
| 193 | else: |
| 194 | dbus_iface = dbus.Interface(dbus_obj, "org.freedesktop.DBus.Properties") |
| 195 | props = dbus_iface.GetAll(iface) |
| 196 | for p in props: |
| 197 | print "{} = {}".format(p, str(props[p])) |
| 198 | result = True |
| 199 | |
| 200 | return result |
| 201 | |
| 202 | def run_all_commands(dbus_bus, recipe, args): |
| 203 | if isinstance(recipe, dict): |
| 204 | return run_one_command(dbus_bus, recipe, args) |
| 205 | |
| 206 | assert isinstance(recipe, list) |
| 207 | for command in recipe: |
| 208 | descriptor = descriptors[command] |
| 209 | if not run_one_command(dbus_bus, descriptor, args): |
| 210 | print "Failed to execute command: {}".format(descriptor) |
| 211 | return False |
| 212 | |
| 213 | return True |
| 214 | |
| 215 | def gpio_set_value(gpio_name, active_low, asserted): |
| 216 | gpio_id = convertGpio(gpio_name) |
| 217 | gpio_value_path = "/sys/class/gpio/gpio{}/value".format(gpio_id) |
| 218 | |
| 219 | with open(gpio_value_path, 'w') as gpio: |
| 220 | # Inversion behaviour needs to change with the resolution of |
| 221 | # https://github.com/openbmc/openbmc/issues/2489, where properly |
| 222 | # configuring the kernel will allow it to handle the inversion for us. |
| 223 | gpio.write(str(int(asserted ^ active_low))) |
| 224 | |
| 225 | return True |
| 226 | |
| 227 | def gpio_deassert(gpio_name, active_low, args): |
| 228 | # Deal with silly python2 exception handling as outlined in main |
| 229 | if args.verbose: |
| 230 | return gpio_set_value(gpio_name, active_low, False) |
| 231 | |
| 232 | try: |
| 233 | return gpio_set_value(gpio_name, active_low, False) |
| 234 | except IOError as e: |
| 235 | print >> sys.stderr, "Failed to access GPIO {}: {}".format(gpio_name, e.message) |
| 236 | return False |
| 237 | |
| 238 | def run_chassiskill(args): |
| 239 | # We shouldn't be able to invoke run_chassiskill() unless it's been |
| 240 | # explicitly added as a valid command to argparse in main() |
| 241 | assert can_chassiskill() |
| 242 | |
| 243 | data = {} |
| 244 | with open(GPIO_DEFS_FILE, 'r') as json_input: |
| 245 | data = json.load(json_input) |
| 246 | |
| 247 | gpios = data['gpio_configs']['power_config']['power_up_outs'] |
| 248 | defs = data['gpio_definitions'] |
| 249 | |
| 250 | for gpio in gpios: |
| 251 | |
| 252 | definition = filter(lambda g: g['name'] == gpio['name'], defs) |
| 253 | if len(definition) == 0: |
| 254 | print >> sys.stderr, "Missing definition for GPIO {}".format(gpio['name']) |
| 255 | continue |
| 256 | |
| 257 | pin = str(definition[0]['pin']) |
| 258 | active_low = not gpio['polarity'] |
| 259 | |
| 260 | if not gpio_deassert(pin, active_low, args): |
| 261 | return False |
| 262 | |
| 263 | return True |
| 264 | |
| 265 | def can_chassiskill(): |
| 266 | |
| 267 | try: |
| 268 | with open(GPIO_DEFS_FILE, 'r') as json_input: |
| 269 | data = json.load(json_input) |
| 270 | gpios = data['gpio_configs']['power_config']['power_up_outs'] |
| 271 | return len(gpios) > 0 |
| 272 | except: |
| 273 | pass |
| 274 | |
| 275 | return False |
| 276 | |
| 277 | def main(): |
| 278 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) |
| 279 | |
| 280 | # Conditionally add the `chassiskill` command based on whether the |
| 281 | # required GPIO configuration is present in the system description. |
| 282 | if can_chassiskill(): |
| 283 | descriptors['chassiskill'] = None |
| 284 | |
| 285 | parser = argparse.ArgumentParser() |
| 286 | parser.add_argument('--verbose', '-v', action='store_true', |
| 287 | help="Verbose output") |
| 288 | parser.add_argument('--wait', '-w', action='store_true', |
| 289 | help='Block until the state transition succeeds or fails') |
| 290 | parser.add_argument('--wait-tune', '-t', nargs='?', default=8, type=float, |
| 291 | # help='Seconds to wait for journal output to complete after receiving DBus signal', |
| 292 | help=argparse.SUPPRESS) |
| 293 | parser.add_argument('recipe', choices=sorted(descriptors.keys())) |
| 294 | args = parser.parse_args() |
| 295 | |
| 296 | # This is a special case: directly pull the power, don't do any D-Bus |
| 297 | # related stuff |
| 298 | if args.recipe == "chassiskill": |
| 299 | return run_chassiskill(args) |
| 300 | |
| 301 | dbus_bus = dbus.SystemBus() |
| 302 | |
| 303 | # The only way to get a sensible backtrace with python 2 without stupid |
| 304 | # hoops is to let the uncaught exception handler do the work for you. |
| 305 | # Catching and binding an exception appears to overwrite the stack trace at |
| 306 | # the point of bind. |
| 307 | # |
| 308 | # So, if we're in verbose mode, don't try to catch the DBus exception. That |
| 309 | # way we can understand where it originated. |
| 310 | if args.verbose: |
| 311 | return run_all_commands(dbus_bus, descriptors[args.recipe], args) |
| 312 | |
| 313 | # Otherwise, we don't care about the traceback. Just catch it and print the |
| 314 | # error message. |
| 315 | try: |
| 316 | return run_all_commands(dbus_bus, descriptors[args.recipe], args) |
| 317 | except dbus.exceptions.DBusException as e: |
| 318 | print >> sys.stderr, "DBus error occurred: {}".format(e.get_dbus_message()) |
| 319 | finally: |
| 320 | dbus_bus.close() |
| 321 | |
| 322 | if __name__ == "__main__": |
| 323 | sys.exit(0 if main() else 1) |