4 libremanage - Lightweight, free software for remote side-chanel server management
6 Copyright (C) 2018 Alyssa Rosenzweig
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.
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.
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/>.
25 $ libremanage [server name] [command]
29 $ libremanage web2 reboot
31 Server names are defined in the accompanying config.py.
33 Valid commands are as follows:
35 - shutdown, reboot, poweron: Power management
36 - tty: Open TTY in GNU Screen
37 - sanity, sanity-sh: SSH sanity tests, ignore
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.
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
]
57 def die_with_usage(message
):
62 def get_server_handle(name
):
64 server
= CONFIG
["servers"][name
]
66 die_with_usage("Unknown server, please configure")
68 # Associate manager configuration
69 server
["ssh"] = CONFIG
["managers"][server
["manager"]]
76 def gpio_export(server
, pin
, mode
):
78 open_ssh(server
, "echo " + str(pin
) + " > /sys/class/gpio/export")
79 open_ssh(server
, "echo out > /sys/class/gpio/gpio" + str(pin
) + "/direction")
81 open_ssh(server
, "echo " + str(pin
) + " > /sys/class/gpio/unexport")
83 def gpio_write(server
, pin
, value
):
84 open_ssh(server
, "echo " + str(value
) + " > /sys/class/gpio/gpio" + str(pin
) + "/value")
86 def power_button(server
, pin
, state
):
87 # Hold down the power to force off (via the EC),
88 # or just flick on to turn on
90 gpio_write(server
, pin
, 1)
91 time
.sleep(2 if state
== POWER_OFF
else 0.5)
92 gpio_write(server
, pin
, 0)
98 def set_server_power(state
, server
):
99 conf
= server
["power"]
102 # Export pin, configure, write value, unexport
105 gpio_export(server
, pin
, True)
107 # Act like a power button
109 if state
== POWER_OFF
or state
== POWER_ON
:
110 power_button(server
, pin
, state
)
111 elif state
== POWER_REBOOT
:
112 # Requires that we already be online.
113 power_button(server
, pin
, POWER_OFF
)
114 power_button(server
, pin
, POWER_ON
)
116 gpio_export(server
, pin
, False)
119 if s
["tty"]["uncolor"]:
120 # Broken serial port, workaround TTY garbage with libremanage-serial
121 subprocess
.run(["libremanage-serial", s
["name"])
123 # Use native GNU screen
124 return open_ssh(s
, "screen " + s
["tty"]["file"] + " " + str(s
["tty"]["baud"]), force_tty
=True),
129 "shutdown": functools
.partial(set_server_power
, POWER_OFF
),
130 "poweron": functools
.partial(set_server_power
, POWER_ON
),
131 "reboot": functools
.partial(set_server_power
, POWER_REBOOT
),
133 # TTY access (or keyboard if wired as such)
136 "tty-baud": lambda s
: open_ssh(s
, "stty -F "+ s
["tty"]["file"] + " " + str(s
["tty"]["baud"])),
137 "tty-read": lambda s
: open_ssh(s
, "cat " + s
["tty"]["file"], force_tty
=True),
138 "tty-write": lambda s
: open_ssh(s
, "stdbuf -o0 cat > " + s
["tty"]["file"], force_tty
=True),
142 "sanity": lambda s
: open_ssh(s
, "whoami"),
143 "sanity-sh": lambda s
: open_ssh(s
, ""),
146 def issue_command(server_name
, command
):
147 server
= get_server_handle(server_name
)
150 callback
= COMMANDS
[command
]
152 die_with_usage("Invalid command supplied")
156 # Load configuration, get command, and go!
159 with
open(os
.path
.expanduser("~/.libremanage.json")) as f
:
160 CONFIG
= json
.load(f
)
161 except FileNotFoundError
:
162 die_with_usage("Configuration file missing in ~/.libremanage.json")
164 if len(sys
.argv
) != 3:
165 die_with_usage("Incorrect number of arguments")
167 issue_command(sys
.argv
[1], sys
.argv
[2])