libremanage 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/python3
  2. """
  3. libremanage - Lightweight, free software for remote side-chanel server management
  4. Copyright (C) 2018 Alyssa Rosenzweig <alyssa@rosenzweig.io>
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU Affero General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU Affero General Public License for more details.
  13. You should have received a copy of the GNU Affero General Public License
  14. along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. """
  16. USAGE = """
  17. Usage:
  18. $ libremanage [server name] [command]
  19. Example:
  20. $ libremanage web2 reboot
  21. Valid commands are as follows:
  22. - shutdown, reboot, poweron: Power management
  23. - tty: Open TTY in GNU Screen
  24. - sanity: Sanity test to check if manager is reachable
  25. - sanity-sh: Sanity test exposing a shell on the manager
  26. Define a configuration file in ~/.libremanage.json. See the included
  27. config.json for an example. Named servers correspond to managed servers;
  28. managers correspond to single-board computers connecting the servers.
  29. libremanage SSHs into the manager to access the server through the
  30. side-channel.
  31. """
  32. import sys
  33. import json
  34. import functools
  35. import subprocess
  36. import time
  37. import os.path
  38. def open_ssh(server, command, force_tty=False):
  39. cfg = server["ssh"]
  40. args = ["ssh"] + (["-t"] if force_tty else []) + [cfg["username"] + "@" + cfg["host"], "-p", str(cfg["port"]), command]
  41. subprocess.run(args)
  42. def die_with_usage(message):
  43. print(message)
  44. print(USAGE)
  45. sys.exit(1)
  46. def get_server_handle(name):
  47. try:
  48. server = CONFIG["servers"][name]
  49. except KeyError:
  50. die_with_usage("Unknown server, please configure")
  51. # Associate manager configuration
  52. server["ssh"] = CONFIG["managers"][server["manager"]]
  53. return server
  54. """
  55. Power management: currently, we only support the `hidusb-relay-cmd` driver,
  56. wired up as to the power button pins. We may want to expose more options in the
  57. config for other boards.
  58. """
  59. POWER_OFF = 0
  60. POWER_ON = 1
  61. POWER_REBOOT = 2
  62. def set_server_power(state, server):
  63. conf = server["power"]
  64. # Ensure the button is in a known state
  65. power_write(server, conf, 0)
  66. if state == POWER_OFF or state == POWER_ON:
  67. power_button(server, conf, state)
  68. elif state == POWER_REBOOT:
  69. # Requires that we already be online.
  70. power_button(server, conf, POWER_OFF)
  71. power_button(server, conf, POWER_ON)
  72. def power_write(server, conf, state):
  73. if conf["type"] == "hidusb-relay-cmd":
  74. verb = "on" if state == 1 else "off"
  75. open_ssh(server, "hidusb-relay-cmd ID=" + conf["relay"] + " " + verb + " " + str(conf["channel"]))
  76. else:
  77. die_with_usage("Unknown power type " + conf["type"])
  78. def power_button(server, conf, state):
  79. # Hold down the power to force off (via the EC),
  80. # or just flick on to turn on
  81. power_write(server, conf, 1)
  82. time.sleep(conf["timing"]["off" if state == POWER_OFF else "on"])
  83. power_write(server, conf, 0)
  84. """
  85. Define the list of commands implemented as a dict mapping names to functions
  86. actuating the command
  87. """
  88. COMMANDS = {
  89. # Power managemment
  90. "shutdown": functools.partial(set_server_power, POWER_OFF),
  91. "poweron": functools.partial(set_server_power, POWER_ON),
  92. "reboot": functools.partial(set_server_power, POWER_REBOOT),
  93. # TTY access (or keyboard if wired as such)
  94. "tty": lambda s: open_ssh(s, "screen " + s["tty"]["file"] + " " + str(s["tty"]["baud"]), force_tty=True),
  95. # SSH sanity tests
  96. "sanity": lambda s: open_ssh(s, "whoami"),
  97. "sanity-sh": lambda s: open_ssh(s, ""),
  98. }
  99. def issue_command(server_name, command):
  100. server = get_server_handle(server_name)
  101. try:
  102. callback = COMMANDS[command]
  103. except KeyError:
  104. die_with_usage("Invalid command supplied")
  105. callback(server)
  106. # Load configuration, get command, and go!
  107. try:
  108. with open(os.path.expanduser("~/.libremanage.json")) as f:
  109. CONFIG = json.load(f)
  110. except FileNotFoundError:
  111. die_with_usage("Configuration file missing in ~/.libremanage.json")
  112. if len(sys.argv) != 3:
  113. die_with_usage("Incorrect number of arguments")
  114. issue_command(sys.argv[1], sys.argv[2])