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
):
63 if name
== "localhost":
67 server
= CONFIG
["servers"][name
]
69 die_with_usage("Unknown server, please configure")
71 # Associate manager configuration
72 server
["ssh"] = CONFIG
["managers"][server
["manager"]]
79 def gpio_export(server
, pin
, 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")
84 open_ssh(server
, "echo " + str(pin
) + " > /sys/class/gpio/unexport")
86 def gpio_write(server
, pin
, value
):
87 open_ssh(server
, "echo " + str(value
) + " > /sys/class/gpio/gpio" + str(pin
) + "/value")
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
93 gpio_write(server
, pin
, 1)
94 time
.sleep(2 if state
== POWER_OFF
else 0.5)
95 gpio_write(server
, pin
, 0)
101 def set_server_power(state
, server
):
102 conf
= server
["power"]
105 # Export pin, configure, write value, unexport
108 gpio_export(server
, pin
, True)
110 # Act like a power button
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
)
119 gpio_export(server
, pin
, False)
122 if s
["tty"]["uncolor"]:
123 # Broken serial port, workaround TTY garbage with libremanage-serial
124 subprocess
.run(["libremanage-serial", s
["name"]])
126 # Use native GNU screen
127 return open_ssh(s
, "screen " + s
["tty"]["file"] + " " + str(s
["tty"]["baud"]), force_tty
=True),
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
),
136 # TTY access (or keyboard if wired as such)
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),
145 "sanity": lambda s
: open_ssh(s
, "whoami"),
146 "sanity-sh": lambda s
: open_ssh(s
, ""),
149 def issue_command(server_name
, command
):
150 server
= get_server_handle(server_name
)
153 callback
= COMMANDS
[command
]
155 die_with_usage("Invalid command supplied")
159 # Load configuration, get command, and go!
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")
167 if len(sys
.argv
) != 3:
168 die_with_usage("Incorrect number of arguments")
170 issue_command(sys
.argv
[1], sys
.argv
[2])