simplify remotes

This commit is contained in:
abrown38 committed 2026-09-30 17:47:41 +00:00
1 parent 02017d470f
commit 998d9f603d
6 files changed
+137 -133

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
- `list` has a new `BOOT` column showing whether each server starts at boot (`enabled`, `disabled`, or e.g. `masked`).
- Manage other hosts, like LXD/Incus remotes:
- `sudo terractl daemon enable` starts `terractld.service`, a daemon other hosts connect to (HTTPS, TCP port 7776; `TERRACTL_LISTEN` in `/etc/default/terractl` changes it). It's off until enabled. `daemon disable` and `daemon status` turn it off and show its state.
- `sudo terractl daemon trust add <name>` creates a one-time token for a computer, valid for 24 hours; `daemon trust list` and `daemon trust remove <name>` show and revoke access. On that computer, `terractl remote add <remote> <token>` adds the host; `remote list` and `remote remove` manage remotes, which are stored per user.
- `daemon enable` prints a command with a one-time token (valid for 24 hours); running it on another computer, `terractl remote add <token>`, adds the host as a remote named after its hostname (or `remote add <name> <token>`). `sudo terractl daemon token` makes tokens for more computers, `daemon status` lists the trusted ones, and `daemon revoke <name>` removes one. Adding a host again replaces the old remote, e.g. after a revoke. `remote list` and `remote remove` manage remotes, which are stored per user.
- `terractl --host <remote> <command>` runs any command on a remote, including `attach`, `watch` and `export`/`import`/`mods add` with files from your computer. The daemon runs it as root there, with messages adjusted to `terractl --host <remote> ...`.
- `terractl copy` and `terractl move` copy, move or rename instances, on one host or between hosts (`[<remote>:]<name>`). Paths and ports follow the destination's settings, and a missing server zip is copied along. `move` stops the source first, starts it again if the transfer fails, and starts the moved instance where it was running before.
- Security: the token carries the daemon's certificate fingerprint, which the client checks on every connection; each computer gets its own key, and the daemon stores only hashes. The daemon runs as root with systemd sandboxing.
+4 -5
View File
@@ -96,13 +96,12 @@ Other hosts:
copy an instance, on this host or between hosts
move [<remote>:]<name> [<remote>:][<new name>] [--with-server]
move (or rename) an instance; stops it while moving
remote add <name> <token> add a host to manage (token from 'daemon trust add')
remote add <name> <address> [<token>] ... when the token's addresses don't work from here
remote add [<name>] <token> [--address <host>[:<port>]]
add a host to manage (token from its 'daemon enable')
remote list | remote remove <name> show or forget remotes
daemon enable|disable|status let other hosts manage this one (terractld.service)
daemon trust add <client> create a one-time token for a computer to join
daemon trust list | trust remove <client>
show or revoke computers that may manage this host
daemon token a one-time token for another computer to add this host
daemon revoke <name> take a computer's access away
Used by terraria@.service:
journal-access let the terraria group read the servers' journal
+1 -3
View File
@@ -153,9 +153,7 @@ _terractl() {
return ;;
daemon)
if (( cword == 2 )); then
_terractl_reply "enable disable status trust"
elif (( cword == 3 )) && [[ ${words[2]} == trust ]]; then
_terractl_reply "add list remove"
_terractl_reply "enable disable status token revoke"
fi
return ;;
esac
+26 -24
View File
@@ -670,7 +670,7 @@ sudo terractl move calamity nas: # this host → nas
terractl copy nas:calamity pi:test # nas → pi, as 'test'
```
### Turning on the daemon
### Setting it up
On the host to be managed:
@@ -678,29 +678,31 @@ On the host to be managed:
sudo terractl daemon enable
```
This starts `terractld.service` and makes it start at boot. It listens on **TCP port 7776**, on all addresses. If a firewall is active, allow that port (for example `sudo ufw allow 7776/tcp`), ideally only from the computers that will manage the host. To listen on one address or on another port, set `TERRACTL_LISTEN` in `/etc/default/terractl` (e.g. `TERRACTL_LISTEN=10.0.10.77:7776`) and run `sudo systemctl restart terractld`.
It prints a command with a one-time token. Run that command on the computer that should manage the host:
`sudo terractl daemon disable` turns it off again. `terractl daemon status` shows whether it's running, where it listens and its certificate's fingerprint, and with `sudo`, which computers it trusts.
```bash
terractl remote add tctl1_eyJ2IjoxLCJmcCI6...
```
That's all: the host is now a remote called by its hostname (say `nas`), and `terractl --host nas list` works. To call it something else, put a name before the token: `terractl remote add nas tctl1_...`. The host, in turn, knows the computer by the computer's hostname.
For each further computer, run `sudo terractl daemon token` on the host for a new token. A token works **once**, **within 24 hours**.
### The daemon
`daemon enable` starts `terractld.service` and makes it start at boot. It listens on **TCP port 7776**, on all addresses. If a firewall is active, allow that port (for example `sudo ufw allow 7776/tcp`), ideally only from the computers that will manage the host. To listen on one address or on another port, set `TERRACTL_LISTEN` in `/etc/default/terractl` (e.g. `TERRACTL_LISTEN=10.0.10.77:7776`) and run `sudo systemctl restart terractld`.
`sudo terractl daemon disable` turns it off again. `terractl daemon status` shows whether it's running, where it listens and its certificate's fingerprint, and with `sudo`, which computers it trusts. `sudo terractl daemon revoke <name>` takes a computer's access away.
The daemon is an ordinary systemd service: systemd starts it at boot, restarts it if it crashes, and keeps its log. `journalctl -u terractld` lists every command it ran and for which computer.
### Adding a remote
### Remotes
Each computer that should manage the host needs a one-time token. On the host with the daemon:
The token contains the host's addresses, so usually `remote add` needs nothing else. If none of them works from the computer (behind NAT, for example), add one that does: `terractl remote add tctl1_... --address 203.0.113.5`, or `--address nas.example.com:7800` for another port.
```bash
sudo terractl daemon trust add laptop
```
Adding a host that's already a remote replaces that remote, keeping its name unless you give a new one. So if a remote stops working because its access was revoked, just get a new token and add it again.
`laptop` is a name for the computer, used in the host's log and to revoke it later. The command prints what to run on that computer:
```bash
terractl remote add nas tctl1_eyJ2IjoxLCJmcCI6...
```
`nas` is the name you'll use for the host there; choose any. The token contains the host's addresses, so usually nothing else is needed. If none of them works from the computer (behind NAT, for example), add the address: `terractl remote add nas 203.0.113.5 tctl1_...`, or `nas.example.com:7800` for another port.
A token works **once**, **within 24 hours**. Remotes are kept per user in `~/.config/terractl/remotes.json`, readable only by you. Under `sudo`, the remotes of the person who ran sudo are used.
Remotes are kept per user in `~/.config/terractl/remotes.json`, readable only by you. Under `sudo`, the remotes of the person who ran sudo are used.
```bash
terractl remote list # your remotes
@@ -757,7 +759,7 @@ On this host, copying *from* it works for members of the `terraria` group. Copyi
- **Encrypted and pinned.** Traffic is HTTPS (TLS 1.2 or newer). The daemon's certificate is self-signed, the token carries its fingerprint, and every connection checks it, so nothing in between can pose as the daemon. If the fingerprint ever changes, `terractl` refuses to connect and says so.
- **Keys, not passwords.** Joining trades the token for a random key for that computer. The daemon keeps only hashes of keys and tokens, in `/var/lib/terractl/trust.json` (root only).
- **A trusted computer is an admin of the host's servers.** It can do what `sudo terractl` can: create, delete, import, start and stop instances, and install mods or server files, which run as the `terraria` user. It can't change the daemon or run anything other than `terractl`. Give tokens only to computers you'd trust with `sudo terractl`.
- **Revoking.** `sudo terractl daemon trust list` shows the trusted computers, and `sudo terractl daemon trust remove laptop` takes one's access away immediately.
- **Revoking.** `sudo terractl daemon status` shows the trusted computers, and `sudo terractl daemon revoke laptop` takes one's access away immediately.
- **Firewall.** The daemon doesn't need to be reachable from the internet. Limit port 7776 to your LAN or VPN, or set `TERRACTL_LISTEN` to a LAN or VPN address.
The daemon runs as root, because it creates and removes instances and starts services, but with systemd's protections it can't write to `/usr`, `/boot`, `/etc` or home folders. Its certificate, key and trust list are in `/var/lib/terractl/`. Files in transit are kept in `/var/lib/terractl/transfers/`, and removed once used, after 6 hours unused, or when the daemon restarts.
@@ -798,7 +800,7 @@ The servers always run as the `terraria` user. Administration is simplest with `
| `import` | root (or `terraria`) | Run as root, it gives the new instance to `terraria`. |
| `prepare`, `run` | Normally only the service | Run as root, they automatically switch to the `terraria` user, so no files end up owned by root. Any other user is refused. |
| `service-stop` | Only the service | Needs the server's process ID from systemd. |
| `daemon enable`, `daemon disable`, `daemon trust ...` | root only | |
| `daemon enable`, `disable`, `token`, `revoke` | root only | |
| `daemon status` | anyone | Shows the trusted computers only with `sudo`. |
| `remote ...`, `--host ...` | anyone | Uses your own remotes. On the remote, the daemon runs the command as root. |
| `copy`, `move` | depends on this host's part | Copying from this host: the group is enough. Copying to it, or moving away from it: root. |
@@ -918,7 +920,7 @@ The daemon isn't running on that host (check with `terractl daemon status` there
The daemon has a different certificate than when you added it. That's expected if `/var/lib/terractl` was deleted there, or the package was purged and installed again; remove the remote and add it again. If nothing like that happened, don't work around it: something may be intercepting the connection.
**`doesn't trust this computer (any more)`.**
Its access was revoked with `daemon trust remove`, or that computer was added again under the same name elsewhere. Get a new token.
Its access was revoked with `daemon revoke`, or the host's `/var/lib/terractl` was reset. Run `sudo terractl daemon token` there and `terractl remote add <token>` here; that replaces the old remote.
---
@@ -972,12 +974,12 @@ terractl copy [<remote>:]<name> [<remote>:][<new name>] [--with-server]
Copy an instance, here or between hosts
terractl move [<remote>:]<name> [<remote>:][<new name>] [--with-server]
Move or rename an instance
terractl remote add <name> <token> Add a remote (token from 'daemon trust add')
terractl remote add <name> <address> [<token>] ... at an address the token doesn't list
terractl remote add [<name>] <token> [--address <host>[:<port>]]
Add (or replace) a remote
terractl remote list | remove <name> Show or forget remotes
terractl daemon enable|disable|status Let other hosts manage this one
terractl daemon trust add|remove <name> Create a token for a computer, or revoke one
terractl daemon trust list Show trusted computers and unused tokens
terractl daemon token A one-time token for another computer
terractl daemon revoke <name> Take a computer's access away
Used by the service:
terractl prepare <name> Create dirs/configs, unpack zip if needed
+104 -99
View File
@@ -14,7 +14,7 @@
# the instance to the destination's folders and free ports.
#
# Security: the daemon's certificate is self-signed. A computer joins with a
# one-time token from 'terractl daemon trust add', which holds the certificate's
# one-time token from 'terractl daemon enable' or 'daemon token', which holds the certificate's
# fingerprint and a secret. The client checks that fingerprint on every
# connection, so nothing in between can pose as the daemon, and trades the
# secret for a random key of its own, sent with each request. The daemon stores
@@ -27,7 +27,6 @@ import base64
import codecs
import errno
import fcntl
import getpass
import hashlib
import hmac
import http.client
@@ -212,7 +211,7 @@ def open_state(cmdline):
class StateLock:
"""Serialises changes to trust.json between the daemon and 'terractl daemon trust'."""
"""Serialises changes to trust.json between the daemon and 'terractl daemon token/revoke'."""
def __enter__(self):
self.fd = os.open(state_path(".lock"), os.O_RDWR | os.O_CREAT, 0o600)
@@ -427,6 +426,15 @@ def extract_upload(tar_path, dest):
raise HttpError(400, f"the files sent aren't a valid archive ({e})")
def unique_client_name(wanted, clients):
base = wanted[:60] if isinstance(wanted, str) and CLIENT_RE.fullmatch(wanted[:60]) else "computer"
taken = {c.get("name") for c in clients}
name, n = base, 2
while name in taken:
name, n = f"{base}-{n}", n + 1
return name
class Handler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
server_version = "terractld"
@@ -579,7 +587,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
# ---- endpoints
def join(self):
secret = self.read_json().get("secret")
req = self.read_json()
secret, wanted, old_key = req.get("secret"), req.get("name"), req.get("old_key")
if not isinstance(secret, str):
raise HttpError(400, "bad request")
digest = sha256(secret)
@@ -593,11 +602,20 @@ class Handler(http.server.BaseHTTPRequestHandler):
save_trust(trust)
dlog(f"{self.client_address[0]}: refused a join (unknown, used or expired token)")
raise HttpError(403, "this token is not valid (it may have been used already, or "
"expired); create a new one there: sudo terractl daemon trust add <name>")
"expired); create a new one there: sudo terractl daemon token")
trust["tokens"].remove(token)
key = secrets.token_urlsafe(32)
name = token["name"]
trust["clients"] = [c for c in trust["clients"] if c.get("name") != name]
# A computer that's trusted already (it sends its old key) keeps its
# entry and name; a new one is named after its hostname
old = None
if isinstance(old_key, str):
old = next((c for c in trust["clients"]
if hmac.compare_digest(c.get("key_sha256", ""), sha256(old_key))), None)
if old:
name = old["name"]
trust["clients"].remove(old)
else:
name = unique_client_name(wanted, trust["clients"])
trust["clients"].append({"name": name, "key_sha256": sha256(key),
"added": time.strftime("%Y-%m-%d %H:%M"),
"from": self.client_address[0]})
@@ -969,11 +987,12 @@ def daemon_enable():
if not wait_listening(host, port, 10):
die(f"terractld isn't listening on {describe_listen(host, port)}; see: journalctl -u terractld")
print(f"terractld is running and starts at boot, listening on {describe_listen(host, port)}.")
print(f"Certificate fingerprint (SHA-256): {cert_fingerprint()}")
print()
print("To let a computer manage this host, create a token for it:")
print(" sudo terractl daemon trust add <name for that computer>")
print(f"If a firewall is active, allow TCP port {port} (e.g. sudo ufw allow {port}/tcp).")
print()
if load_trust()["clients"]:
print("To add another computer: sudo terractl daemon token")
else:
print_token()
return 0
@@ -983,7 +1002,7 @@ def daemon_disable():
if proc.returncode != 0:
die(f"stopping terractld failed: {proc.stderr.strip()}")
print("terractld is stopped and won't start at boot, so other hosts can't manage this one.")
print("The computers it trusts are kept for next time (terractl daemon trust list).")
print("The computers it trusts are kept for next time (sudo terractl daemon status).")
return 0
@@ -1001,76 +1020,54 @@ def daemon_status():
except PermissionError:
print("trusted: (run with sudo to see which computers)")
trust = None
if trust is not None:
names = ", ".join(c["name"] for c in trust["clients"]) or "none yet"
print(f"trusted: {names}")
waiting = [t["name"] for t in trust["tokens"] if t["expires"] > time.time()]
if waiting:
print(f"tokens for: {', '.join(waiting)} (not used yet)")
if trust is not None and not trust["clients"]:
print("trusted: none yet (add one: sudo terractl daemon token)")
elif trust is not None:
print()
print(f"{'TRUSTED':<24} {'ADDED':<17} FROM")
for c in trust["clients"]:
print(f"{c['name']:<24} {c.get('added', '-'):<17} {c.get('from', '-')}")
unused = [t for t in trust["tokens"] if t["expires"] > time.time()]
if unused:
print(f"(and {len(unused)} unused token{'s' if len(unused) > 1 else ''})")
if active != "active":
print("Start it with: sudo terractl daemon enable")
return 0
def trust_add(name):
if not CLIENT_RE.fullmatch(name):
die(f"invalid name '{name}' (letters, digits and _ . - @, up to 64)")
open_state(f"daemon trust add {name}")
def print_token():
"""Creates a one-time token and prints the command that uses it."""
open_state("daemon token")
ensure_cert()
secret = secrets.token_hex(32)
with StateLock():
trust = load_trust()
now = time.time()
trust["tokens"] = [t for t in trust["tokens"] if t["expires"] > now and t["name"] != name]
trust["tokens"].append({"name": name, "secret_sha256": sha256(secret),
"expires": int(now + TOKEN_HOURS * 3600)})
trust["tokens"] = [t for t in trust["tokens"] if t["expires"] > now]
trust["tokens"].append({"secret_sha256": sha256(secret), "expires": int(now + TOKEN_HOURS * 3600)})
save_trust(trust)
known = any(c["name"] == name for c in trust["clients"])
host, port = listen_setting()
token = make_token({"v": 1, "fp": cert_fingerprint(), "s": secret, "a": host_addresses(host, port)})
fields = {"v": 1, "fp": cert_fingerprint(), "s": secret, "a": host_addresses(host, port)}
here = (socket.gethostname() or "").split(".")[0]
if not NAME_RE.fullmatch(here):
here = "<name>"
print(f"On '{name}', run this to add this host as a remote called '{here}':")
if NAME_RE.fullmatch(here):
fields["n"] = here # the remote's name, unless the other computer picks one
print(f"To manage this host from another computer, run this there (works once, within {TOKEN_HOURS} hours):")
print()
print(f" terractl remote add {here} {token}")
print(f" terractl remote add {make_token(fields)}")
print()
print(f"The token works once, within {TOKEN_HOURS} hours.")
if known:
print(f"'{name}' is trusted already; joining again gives it a new key and the old one stops working.")
if systemctl("is-active", DAEMON_UNIT).stdout.strip() != "active":
print("The daemon isn't running yet; start it with: sudo terractl daemon enable")
return 0
def trust_list():
try:
trust = load_trust()
except PermissionError:
die("this needs root: sudo terractl daemon trust list")
if not trust["clients"]:
print("No computers are trusted yet. Add one with: sudo terractl daemon trust add <name>")
else:
print(f"{'NAME':<24} {'ADDED':<17} FROM")
for c in trust["clients"]:
print(f"{c['name']:<24} {c.get('added', '-'):<17} {c.get('from', '-')}")
now = time.time()
for t in trust["tokens"]:
if t["expires"] > now:
hours = max(1, round((t["expires"] - now) / 3600))
print(f"Token for '{t['name']}' not used yet (expires in {hours} h)")
return 0
def trust_remove(name):
open_state(f"daemon trust remove {name}")
def daemon_revoke(name):
open_state(f"daemon revoke {name}")
with StateLock():
trust = load_trust()
clients = [c for c in trust["clients"] if c["name"] != name]
tokens = [t for t in trust["tokens"] if t["name"] != name]
if len(clients) == len(trust["clients"]) and len(tokens) == len(trust["tokens"]):
die(f"'{name}' isn't trusted (see: terractl daemon trust list)")
trust["clients"], trust["tokens"] = clients, tokens
if len(clients) == len(trust["clients"]):
die(f"no computer called '{name}' is trusted (see: sudo terractl daemon status)")
trust["clients"] = clients
save_trust(trust)
print(f"'{name}' can't manage this host any more.")
return 0
@@ -1078,20 +1075,13 @@ def trust_remove(name):
def daemon_main(argv):
sub, rest = (argv[0], argv[1:]) if argv else ("", [])
if sub == "trust":
what = rest[0] if rest else "list"
if what == "add" and len(rest) == 2:
return trust_add(rest[1])
if what == "list" and len(rest) <= 1:
return trust_list()
if what == "remove" and len(rest) == 2:
return trust_remove(rest[1])
die("usage: terractl daemon trust add <name> | trust list | trust remove <name>")
if sub == "revoke" and len(rest) == 1:
return daemon_revoke(rest[0])
commands = {"enable": daemon_enable, "disable": daemon_disable, "status": daemon_status,
"serve": daemon_serve}
"token": print_token, "serve": daemon_serve}
if sub in commands and not rest:
return commands[sub]()
die("usage: terractl daemon enable | disable | status | trust ...")
die("usage: terractl daemon enable | disable | status | token | revoke <name>")
# --------------------------------------------------------------- client ----
@@ -1244,9 +1234,8 @@ class Remote:
except (ValueError, KeyError, TypeError):
message = f"HTTP error {resp.status}"
if resp.status == 401:
die(f"{self.label} doesn't trust this computer (any more). On {self.label}, run "
f"'sudo terractl daemon trust add <name for this computer>', then here: terractl "
f"remote remove {self.name} && terractl remote add {self.name} {self.addr} <token>")
die(f"{self.label} doesn't trust this computer (any more). Run 'sudo terractl daemon "
f"token' there, then here: terractl remote add <token>")
die(f"{self.label}: {message}")
return resp
@@ -1384,40 +1373,50 @@ def find_remote(spec):
if host and conf["address"].lower() == host.lower() and int(conf["port"]) == port:
return Remote(name, conf, via=f"--host {spec}")
die(f"'{spec}' isn't one of your remotes (see: terractl remote list). To add it, run "
f"'sudo terractl daemon trust add <name for this computer>' there, then here: "
f"terractl remote add <name> <token>")
f"'sudo terractl daemon token' there, then here: terractl remote add <token>")
def remote_add(args):
if len(args) not in (2, 3):
die("usage: terractl remote add <name> <token>, or: terractl remote add <name> <address> [<token>]")
name = args[0]
usage = "usage: terractl remote add [<name>] <token> [--address <host>[:<port>]]"
address, rest = None, []
args = iter(args)
for arg in args:
if arg == "--address":
address = next(args, None) or die(usage)
elif arg.startswith("--address="):
address = arg.split("=", 1)[1]
elif arg.startswith("-"):
die(f"unknown option '{arg}'")
else:
rest.append(arg)
if len(rest) not in (1, 2):
die(usage)
name, token = (None, rest[0]) if len(rest) == 1 else rest
fields = read_token(token)
remotes = load_remotes()
# Adding a host that's a remote already (same certificate) replaces that
# remote: after its access was revoked, say, or to rename it
old = next((n for n, c in sorted(remotes.items()) if c["fingerprint"] == fields["fp"]), None)
name = name or old or fields.get("n")
if not name:
die("give the remote a name: terractl remote add <name> <token>")
if not NAME_RE.fullmatch(name):
die(f"invalid remote name '{name}' (letters, digits and _ . -, not starting with . or -)")
remotes = load_remotes()
if name in remotes:
die(f"there's a remote called '{name}' already (to replace it: terractl remote remove {name})")
if len(args) == 3:
address, token = args[1], args[2]
elif args[1].startswith(TOKEN_PREFIX):
address, token = None, args[1]
else:
address = args[1]
if not sys.stdin.isatty():
die("no token given")
token = getpass.getpass("Token (from 'sudo terractl daemon trust add' on that host): ")
fields = read_token(token)
if name in remotes and name != old:
die(f"'{name}' is already the name of another remote; choose another: terractl remote add <name> <token>")
candidates = [split_hostport(address)] if address else [split_hostport(a) for a in fields.get("a", [])]
if not candidates:
die("the token doesn't say where the host is; give its address: "
"terractl remote add <name> <address> <token>")
die("the token doesn't say where the host is; add --address <host>")
join = {"secret": fields["s"], "name": (socket.gethostname() or "").split(".")[0]}
if old:
join["old_key"] = remotes[old].get("key", "")
problems = []
for host, port in candidates:
remote = Remote(name, {"address": host, "port": port, "fingerprint": fields["fp"]})
remote.joining = True
remote.label = remote.addr
try:
reply = remote.call("POST", "/1.0/trust/join", {"secret": fields["s"]}, auth=False, timeout=5)
reply = remote.call("POST", "/1.0/trust/join", join, auth=False, timeout=5)
break
except Unreachable as e:
problems.append(str(e))
@@ -1425,7 +1424,9 @@ def remote_add(args):
if len(problems) == 1:
die(problems[0])
die("couldn't reach the host at any address in the token:\n " + "\n ".join(problems) +
"\nGive an address that works from here: terractl remote add <name> <address> <token>")
"\nGive an address that works from here: terractl remote add <token> --address <host>")
if old:
del remotes[old]
remotes[name] = {"address": host, "port": port, "fingerprint": fields["fp"], "key": reply["key"],
"client_name": reply["name"], "added": time.strftime("%Y-%m-%d %H:%M")}
save_remotes(remotes)
@@ -1437,7 +1438,7 @@ def remote_add(args):
about = f"{info['hostname']} at {remote.addr}, terractl {info['version']}"
except Fail:
about = remote.addr
print(f"added remote '{name}' ({about}); it knows this computer as '{reply['name']}'")
print(f"{'updated' if old else 'added'} remote '{name}' ({about}); it knows this computer as '{reply['name']}'")
print(f"Try: terractl --host {name} list")
return 0
@@ -1447,7 +1448,7 @@ def remote_list(args):
die("usage: terractl remote list")
remotes = load_remotes()
if not remotes:
print("No remotes yet. Add one with: terractl remote add <name> <token>")
print("No remotes yet. Run 'sudo terractl daemon token' on a host, then here: terractl remote add <token>")
return 0
print(f"{'NAME':<20} {'ADDRESS':<30} FINGERPRINT")
for name, conf in sorted(remotes.items()):
@@ -1463,8 +1464,8 @@ def remote_remove(args):
if conf is None:
die(f"there's no remote called '{args[0]}' (see: terractl remote list)")
save_remotes(remotes)
print(f"removed remote '{args[0]}'. It still lists this computer as '{conf.get('client_name', '?')}'; "
f"to revoke that too, run there: sudo terractl daemon trust remove {conf.get('client_name', '<name>')}")
print(f"removed remote '{args[0]}'. It still trusts this computer; to revoke that too, run there: "
f"sudo terractl daemon revoke {conf.get('client_name', '<name>')}")
return 0
@@ -1940,6 +1941,10 @@ if __name__ == "__main__":
except Fail as e:
print(f"terractl: {e}", file=sys.stderr)
sys.exit(1)
except BrokenPipeError:
# Output went into something that stopped reading (e.g. head)
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
sys.exit(1)
except OSError as e:
where = f": {e.filename}" if e.filename else ""
print(f"terractl: {e.strerror or e}{where}", file=sys.stderr)
+1 -1
View File
@@ -8,7 +8,7 @@ Wants=network-online.target
Type=simple
# Listens on TERRACTL_LISTEN from /etc/default/terractl (default: all addresses,
# port 7776). Its certificate and the list of trusted computers are kept in
# /var/lib/terractl. Manage it with: terractl daemon enable|disable|status|trust
# /var/lib/terractl. Manage it with: terractl daemon enable|disable|status|token|revoke
ExecStart=@BINDIR@/terractl daemon serve
Restart=on-failure
RestartSec=5