Add hidusb relay instructions
[libremanage.git] / libremanage
1 #!/usr/bin/python3
2
3 """
4 libremanage - Lightweight, free software for remote side-chanel server management
5
6 Copyright (C) 2018 Alyssa Rosenzweig
7
8 This program is free software: you can redistribute it and/or modify
9 it under the terms of the GNU Affero General Public License as published by
10 the Free Software Foundation, either version 3 of the License, or
11 (at your option) any later version.
12
13 This program is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU Affero General Public License for more details.
17
18 You should have received a copy of the GNU Affero General Public License
19 along with this program. If not, see <https://www.gnu.org/licenses/>.
20 """
21
22 USAGE = """
23 Usage:
24
25 $ libremanage [server name] [command]
26
27 Example:
28
29 $ libremanage web2 reboot
30
31 Server names are defined in the accompanying config.py.
32
33 Valid commands are as follows:
34
35 - shutdown, reboot, poweron: Power management
36 - tty: Open TTY in GNU Screen
37 - sanity, sanity-sh: SSH sanity tests, ignore
38
39 Define a configuration file in ~/.libremanage.json. See the included
40 config.json for an example. Servers correspond to managed servers; managers
41 correspond to single-board computers connecting the servers. libremanage SSHs
42 into the manager to access the server through the side-channel.
43 """
44
45 import sys
46 import json
47 import functools
48 import subprocess
49 import time
50 import os.path
51
52 def open_ssh(server, command, force_tty=False):
53 config = server["ssh"]
54 args = ["ssh"] + (["-t"] if force_tty else []) + [config["username"] + "@" + config["host"], "-p", str(config["port"]), command]
55 subprocess.run(args)
56
57 def die_with_usage(message):
58 print(message)
59 print(USAGE)
60 sys.exit(1)
61
62 def get_server_handle(name):
63 if name == "localhost":
64 return
65
66 try:
67 server = CONFIG["servers"][name]
68 except KeyError:
69 die_with_usage("Unknown server, please configure")
70
71 # Associate manager configuration
72 server["ssh"] = CONFIG["managers"][server["manager"]]
73
74 # Meta access
75 server["name"] = name
76
77 return server
78
79 def gpio_export(server, pin, mode):
80 if mode:
81 open_ssh(server, "echo " + str(pin) + " > /sys/class/gpio/export")
82 open_ssh(server, "echo out > /sys/class/gpio/gpio" + str(pin) + "/direction")
83 else:
84 open_ssh(server, "echo " + str(pin) + " > /sys/class/gpio/unexport")
85
86 def gpio_write(server, pin, value):
87 open_ssh(server, "echo " + str(value) + " > /sys/class/gpio/gpio" + str(pin) + "/value")
88
89 def power_button(server, pin, state):
90 # Hold down the power to force off (via the EC),
91 # or just flick on to turn on
92
93 gpio_write(server, pin, 1)
94 time.sleep(2 if state == POWER_OFF else 0.5)
95 gpio_write(server, pin, 0)
96
97 POWER_OFF = 0
98 POWER_ON = 1
99 POWER_REBOOT = 2
100
101 def set_server_power(state, server):
102 conf = server["power"]
103
104 # TODO: Invert
105 # Export pin, configure, write value, unexport
106 pin = conf["pin"]
107
108 gpio_export(server, pin, True)
109
110 # Act like a power button
111
112 if state == POWER_OFF or state == POWER_ON:
113 power_button(server, pin, state)
114 elif state == POWER_REBOOT:
115 # Requires that we already be online.
116 power_button(server, pin, POWER_OFF)
117 power_button(server, pin, POWER_ON)
118
119 gpio_export(server, pin, False)
120
121 def open_tty(s):
122 if s["tty"]["uncolor"]:
123 # Broken serial port, workaround TTY garbage with libremanage-serial
124 subprocess.run(["libremanage-serial", s["name"]])
125 else:
126 # Use native GNU screen
127 return open_ssh(s, "screen " + s["tty"]["file"] + " " + str(s["tty"]["baud"]), force_tty=True),
128
129 COMMANDS = {
130 # Power managemment
131
132 "shutdown": functools.partial(set_server_power, POWER_OFF),
133 "poweron": functools.partial(set_server_power, POWER_ON),
134 "reboot": functools.partial(set_server_power, POWER_REBOOT),
135
136 # TTY access (or keyboard if wired as such)
137
138 "tty": open_tty,
139 "tty-baud": lambda s: open_ssh(s, "stty -F "+ s["tty"]["file"] + " " + str(s["tty"]["baud"])),
140 "tty-read": lambda s: open_ssh(s, "cat " + s["tty"]["file"], force_tty=True),
141 "tty-write": lambda s: open_ssh(s, "stdbuf -o0 cat > " + s["tty"]["file"], force_tty=True),
142
143 # SSH sanity tests
144
145 "sanity": lambda s: open_ssh(s, "whoami"),
146 "sanity-sh": lambda s: open_ssh(s, ""),
147 }
148
149 def issue_command(server_name, command):
150 server = get_server_handle(server_name)
151
152 try:
153 callback = COMMANDS[command]
154 except KeyError:
155 die_with_usage("Invalid command supplied")
156
157 callback(server)
158
159 # Load configuration, get command, and go!
160
161 try:
162 with open(os.path.expanduser("~/.libremanage.json")) as f:
163 CONFIG = json.load(f)
164 except FileNotFoundError:
165 die_with_usage("Configuration file missing in ~/.libremanage.json")
166
167 if len(sys.argv) != 3:
168 die_with_usage("Incorrect number of arguments")
169
170 issue_command(sys.argv[1], sys.argv[2])