lib.state_helpers

  1import shlex
  2from ipaddress import IPv4Address, IPv4Interface
  3
  4from SRE.lib_sre import NetScheme0
  5
  6
  7def set_unbound_server(net_scheme: NetScheme0, machine: str):
  8    """Write a permissive Unbound DNS config and start the service on *machine*."""
  9    set_basic_unbound_server(net_scheme=net_scheme, machine=machine)
 10
 11
 12def set_basic_unbound_server(net_scheme: NetScheme0, machine: str):
 13    """Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on *machine*."""
 14    net_scheme.file(machine=machine, filename='/etc/unbound/unbound.conf', content="""
 15# Unbound configuration file for Debian.
 16#
 17# See the unbound.conf(5) man page.
 18#
 19# See /usr/share/doc/unbound/examples/unbound.conf for a commented
 20# reference config file.
 21#
 22# The following line includes additional configuration files from the
 23# /etc/unbound/unbound.conf.d directory.
 24include-toplevel: "/etc/unbound/unbound.conf.d/*.conf"
 25server:
 26    interface: 0.0.0.0
 27    access-control: 0.0.0.0/0 allow
 28""")
 29    net_scheme.cmd(machine, "systemctl start unbound")
 30
 31
 32def set_nat_gateway(net_scheme: NetScheme0, machine: str):
 33    """Add an iptables MASQUERADE rule on *machine*'s bridged interface.
 34
 35    The machine must have been declared with ``bridged=True``; Kathara appends
 36    the bridged interface as the next ``eth{N}`` after all topology-defined
 37    adapters (i.e. its index equals the highest assigned interface number + 1).
 38    """
 39    m = net_scheme.get_machine(machine)
 40    iface_numbers = [a.interface for a in m.net_adapters.values()]
 41    bridged_iface = (max(iface_numbers) + 1) if iface_numbers else 0
 42    net_scheme.cmd(machine,
 43                   f"sh -c 'iptables -t nat -C POSTROUTING -o eth{bridged_iface} -j MASQUERADE 2>/dev/null"
 44                   f" || iptables -t nat -A POSTROUTING -o eth{bridged_iface} -j MASQUERADE'")
 45
 46
 47def hosts_file_content(net_scheme: NetScheme0, domain_extension: str, included=None, ips=None,
 48                       separator: str = "\t\t") -> str:
 49    """Return /etc/hosts lines for the given machines.
 50
 51    Args:
 52        net_scheme: the NetScheme0 instance
 53        domain_extension: domain suffix (e.g. 'example.com')
 54        included: list of machine names; defaults to get_visibles_machines()
 55        ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network,
 56             in the same order as host_interfaces_from_topology().
 57             If None, addresses are read from net_scheme.data.ips.*
 58        separator: string placed between fields (default: two tabs)
 59    """
 60    if included is None:
 61        included = [m.name for m in net_scheme.get_visibles_machines()]
 62
 63    machine_nets = net_scheme.host_interfaces_from_topology()
 64    lines = []
 65
 66    for machine_name in included:
 67        nets = machine_nets.get(machine_name, [])
 68        single = len(nets) == 1
 69
 70        if ips is not None:
 71            addrs = ips.get(machine_name, [])
 72            for i, net_name in enumerate(nets):
 73                ip = str(addrs[i]).split('/')[0] if i < len(addrs) else None
 74                if ip is None:
 75                    continue
 76                if single:
 77                    lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}")
 78                else:
 79                    lines.append(
 80                        f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}")
 81        else:
 82            for net_name in nets:
 83                attr = machine_name if single else f"{machine_name}_{net_name}"
 84                ip_obj = getattr(net_scheme.data.ips, attr, None)
 85                if ip_obj is None:
 86                    continue
 87                ip = str(ip_obj).split('/')[0]
 88                if single:
 89                    lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}")
 90                else:
 91                    lines.append(
 92                        f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}")
 93
 94    return '\n'.join(lines) + '\n' if lines else ''
 95
 96
 97def create_hosts_file(net_scheme: NetScheme0, domain_extension: str, machine_list=None, included=None, ips=None,
 98                      separator: str = "\t\t"):
 99    """Write /etc/hosts to each machine in machine_list.
100
101    Each file starts with the standard loopback entries (127.0.0.1 localhost and
102    127.0.1.1 for the machine itself), followed by the lines produced by
103    hosts_file_content() for the machines in included.
104
105    Args:
106        net_scheme: the NetScheme0 instance
107        domain_extension: domain suffix appended to every hostname (e.g. 'example.com')
108        machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines()
109        included: machines whose entries appear in the hosts table; passed through to
110                  hosts_file_content() — defaults to get_visibles_machines() when None
111        ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content()
112        separator: string placed between fields (default: two tabs)
113    """
114    if machine_list is None:
115        machine_list = included if included is not None else [m.name for m in net_scheme.get_visibles_machines()]
116
117    hosts = hosts_file_content(net_scheme=net_scheme, domain_extension=domain_extension, included=included, ips=ips,
118                               separator=separator)
119
120    for m in machine_list:
121        hosts_start = f"127.0.0.1\t\tlocalhost\n127.0.1.1\t\t{m}\t\t{m}.{domain_extension}\n"
122        net_scheme.file(machine=m, filename='/etc/hosts', content=hosts_start + hosts, permissions=0o0644,
123                        owner="root:root")
124
125
126def change_password(net_scheme: NetScheme0, machine: str, username: str, password: str):
127    """Set *username*'s password on *machine* via chpasswd.
128
129    The password is written to a temporary file (never passed on the command line).
130    """
131    # Write "username:password" to a file so the password never appears in a shell command.
132    net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd',
133                    content=f'{username}:{password}\n', permissions=0o600)
134    net_scheme.cmd(machine, 'sh -c "chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd"')
135
136
137def create_user(net_scheme: NetScheme0, machine: str, username: str, password: str, uid: int = None, gid: int = None, shell: str = "/bin/bash"):
138    """Create *username* on *machine* (if not already present) and set its password.
139
140    Uses ``useradd -m`` with optional *uid*/*gid* and login *shell*.  The password is written to a
141    temporary file; the username is passed via an environment variable to prevent shell injection.
142    """
143    # Write "username:password" to a file so the password never appears in a shell command.
144    net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd',
145                    content=f'{username}:{password}\n', permissions=0o600)
146    useradd_opts = f" -s {shlex.quote(shell)}"
147    if uid is not None:
148        useradd_opts += f" -u {int(uid)}"
149    if gid is not None:
150        useradd_opts += f" -g {int(gid)}"
151    # Pass the username via an env var so no user-controlled text appears inside the sh -c string.
152    # "$SRE_USER" is double-quoted in the shell command to prevent word-splitting and glob expansion.
153    net_scheme.cmd(machine,
154                   f"env SRE_USER={shlex.quote(username)} "
155                   f"sh -c 'id \"$SRE_USER\" >/dev/null 2>&1"
156                   f" || useradd{useradd_opts} -m -k /etc/skel \"$SRE_USER\";"
157                   f" chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd'")
158
159
160def setup_simple_tcp_server(net_scheme: NetScheme0, machine: str, port: int, answer: str,
161                            ip: "str | IPv4Interface | IPv4Address" = None):
162    """Setup and (re)launch an idempotent TCP server on *machine*.
163
164    The server listens on *port* — bound to *ip* if provided (the network prefix of an
165    ``IPv4Interface`` is stripped), or to ``0.0.0.0`` otherwise. On each client connection
166    it sends *answer* (UTF-8) and closes the socket. Calling this function again for the
167    same *port* kills the previous instance before relaunching.
168    """
169    if ip is None:
170        bind_addr = "0.0.0.0"
171    elif isinstance(ip, IPv4Interface):
172        bind_addr = str(ip.ip)
173    else:
174        bind_addr = str(ip).split('/')[0]
175
176    script_path = f"/usr/local/sbin/sre_tcp_server_{port}.py"
177    answer_file = f"/var/lib/sre_tcp_server_{port}.answer"
178    log_file = f"/var/log/sre_tcp_server_{port}.log"
179    pid_file = f"/run/sre_tcp_server_{port}.pid"
180
181    net_scheme.file(machine=machine, filename=answer_file, content=answer, permissions=0o644)
182
183    # The script double-forks AND closes the stdio fds inherited from docker exec —
184    # otherwise exec_run keeps streaming and the state op hangs forever. After the
185    # second fork, fds 0/1/2 are reopened on /dev/null (stdin) and the per-port log
186    # file (stdout/stderr), so binding/startup failures land in the log. The daemon
187    # also writes its PID to a per-port file so the launcher can kill the previous
188    # instance without using `pkill -f` (which would match the launcher's own sh -c
189    # argument and kill the shell before the python3 command runs).
190    script_content = (
191        "#!/usr/bin/env python3\n"
192        "import os, socket, traceback\n"
193        "if os.fork() != 0: os._exit(0)\n"
194        "os.setsid()\n"
195        "if os.fork() != 0: os._exit(0)\n"
196        "# detach from docker exec's stdio so exec_run can return\n"
197        "for fd in (0, 1, 2):\n"
198        "    try: os.close(fd)\n"
199        "    except OSError: pass\n"
200        "os.open(os.devnull, os.O_RDONLY)  # fd 0\n"
201        f"_log = os.open({log_file!r}, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)  # fd 1\n"
202        "os.dup2(_log, 2)  # fd 2 = log\n"
203        f"with open({pid_file!r}, 'w') as _pf: _pf.write(str(os.getpid()))\n"
204        "try:\n"
205        f"    with open({answer_file!r}, 'rb') as f:\n"
206        "        answer = f.read()\n"
207        "    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n"
208        "    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n"
209        f"    s.bind(({bind_addr!r}, {int(port)}))\n"
210        "    s.listen(16)\n"
211        "    while True:\n"
212        "        conn, _ = s.accept()\n"
213        "        try:\n"
214        "            conn.sendall(answer)\n"
215        "        finally:\n"
216        "            conn.close()\n"
217        "except Exception:\n"
218        "    traceback.print_exc()\n"
219        "    os._exit(1)\n"
220    )
221    net_scheme.file(machine=machine, filename=script_path, content=script_content, permissions=0o755)
222
223    quoted_script = shlex.quote(script_path)
224    quoted_pidfile = shlex.quote(pid_file)
225    # If a previous instance left a PID file, kill it and give the kernel a moment
226    # to release the port; ignore stale PIDs. Then launch the new daemon. The script
227    # daemonizes itself, so this command returns immediately.
228    net_scheme.cmd(machine,
229                   f"sh -c '[ -f {quoted_pidfile} ] && kill $(cat {quoted_pidfile}) 2>/dev/null; "
230                   f"sleep 0.2; "
231                   f"python3 {quoted_script}'")
def set_unbound_server(net_scheme: SRE.lib_sre.NetScheme0, machine: str):
 8def set_unbound_server(net_scheme: NetScheme0, machine: str):
 9    """Write a permissive Unbound DNS config and start the service on *machine*."""
10    set_basic_unbound_server(net_scheme=net_scheme, machine=machine)

Write a permissive Unbound DNS config and start the service on machine.

def set_basic_unbound_server(net_scheme: SRE.lib_sre.NetScheme0, machine: str):
13def set_basic_unbound_server(net_scheme: NetScheme0, machine: str):
14    """Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on *machine*."""
15    net_scheme.file(machine=machine, filename='/etc/unbound/unbound.conf', content="""
16# Unbound configuration file for Debian.
17#
18# See the unbound.conf(5) man page.
19#
20# See /usr/share/doc/unbound/examples/unbound.conf for a commented
21# reference config file.
22#
23# The following line includes additional configuration files from the
24# /etc/unbound/unbound.conf.d directory.
25include-toplevel: "/etc/unbound/unbound.conf.d/*.conf"
26server:
27    interface: 0.0.0.0
28    access-control: 0.0.0.0/0 allow
29""")
30    net_scheme.cmd(machine, "systemctl start unbound")

Write /etc/unbound/unbound.conf (listen on 0.0.0.0, allow all) and start unbound on machine.

def set_nat_gateway(net_scheme: SRE.lib_sre.NetScheme0, machine: str):
33def set_nat_gateway(net_scheme: NetScheme0, machine: str):
34    """Add an iptables MASQUERADE rule on *machine*'s bridged interface.
35
36    The machine must have been declared with ``bridged=True``; Kathara appends
37    the bridged interface as the next ``eth{N}`` after all topology-defined
38    adapters (i.e. its index equals the highest assigned interface number + 1).
39    """
40    m = net_scheme.get_machine(machine)
41    iface_numbers = [a.interface for a in m.net_adapters.values()]
42    bridged_iface = (max(iface_numbers) + 1) if iface_numbers else 0
43    net_scheme.cmd(machine,
44                   f"sh -c 'iptables -t nat -C POSTROUTING -o eth{bridged_iface} -j MASQUERADE 2>/dev/null"
45                   f" || iptables -t nat -A POSTROUTING -o eth{bridged_iface} -j MASQUERADE'")

Add an iptables MASQUERADE rule on machine's bridged interface.

The machine must have been declared with bridged=True; Kathara appends the bridged interface as the next eth{N} after all topology-defined adapters (i.e. its index equals the highest assigned interface number + 1).

def hosts_file_content( net_scheme: SRE.lib_sre.NetScheme0, domain_extension: str, included=None, ips=None, separator: str = '\t\t') -> str:
48def hosts_file_content(net_scheme: NetScheme0, domain_extension: str, included=None, ips=None,
49                       separator: str = "\t\t") -> str:
50    """Return /etc/hosts lines for the given machines.
51
52    Args:
53        net_scheme: the NetScheme0 instance
54        domain_extension: domain suffix (e.g. 'example.com')
55        included: list of machine names; defaults to get_visibles_machines()
56        ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network,
57             in the same order as host_interfaces_from_topology().
58             If None, addresses are read from net_scheme.data.ips.*
59        separator: string placed between fields (default: two tabs)
60    """
61    if included is None:
62        included = [m.name for m in net_scheme.get_visibles_machines()]
63
64    machine_nets = net_scheme.host_interfaces_from_topology()
65    lines = []
66
67    for machine_name in included:
68        nets = machine_nets.get(machine_name, [])
69        single = len(nets) == 1
70
71        if ips is not None:
72            addrs = ips.get(machine_name, [])
73            for i, net_name in enumerate(nets):
74                ip = str(addrs[i]).split('/')[0] if i < len(addrs) else None
75                if ip is None:
76                    continue
77                if single:
78                    lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}")
79                else:
80                    lines.append(
81                        f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}")
82        else:
83            for net_name in nets:
84                attr = machine_name if single else f"{machine_name}_{net_name}"
85                ip_obj = getattr(net_scheme.data.ips, attr, None)
86                if ip_obj is None:
87                    continue
88                ip = str(ip_obj).split('/')[0]
89                if single:
90                    lines.append(f"{ip}{separator}{machine_name}{separator}{machine_name}.{domain_extension}")
91                else:
92                    lines.append(
93                        f"{ip}{separator}{machine_name}_{net_name}{separator}{machine_name}_{net_name}.{domain_extension}")
94
95    return '\n'.join(lines) + '\n' if lines else ''

Return /etc/hosts lines for the given machines.

Arguments:
  • net_scheme: the NetScheme0 instance
  • domain_extension: domain suffix (e.g. 'example.com')
  • included: list of machine names; defaults to get_visibles_machines()
  • ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} one ip per network, in the same order as host_interfaces_from_topology(). If None, addresses are read from net_scheme.data.ips.*
  • separator: string placed between fields (default: two tabs)
def create_hosts_file( net_scheme: SRE.lib_sre.NetScheme0, domain_extension: str, machine_list=None, included=None, ips=None, separator: str = '\t\t'):
 98def create_hosts_file(net_scheme: NetScheme0, domain_extension: str, machine_list=None, included=None, ips=None,
 99                      separator: str = "\t\t"):
100    """Write /etc/hosts to each machine in machine_list.
101
102    Each file starts with the standard loopback entries (127.0.0.1 localhost and
103    127.0.1.1 for the machine itself), followed by the lines produced by
104    hosts_file_content() for the machines in included.
105
106    Args:
107        net_scheme: the NetScheme0 instance
108        domain_extension: domain suffix appended to every hostname (e.g. 'example.com')
109        machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines()
110        included: machines whose entries appear in the hosts table; passed through to
111                  hosts_file_content() — defaults to get_visibles_machines() when None
112        ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content()
113        separator: string placed between fields (default: two tabs)
114    """
115    if machine_list is None:
116        machine_list = included if included is not None else [m.name for m in net_scheme.get_visibles_machines()]
117
118    hosts = hosts_file_content(net_scheme=net_scheme, domain_extension=domain_extension, included=included, ips=ips,
119                               separator=separator)
120
121    for m in machine_list:
122        hosts_start = f"127.0.0.1\t\tlocalhost\n127.0.1.1\t\t{m}\t\t{m}.{domain_extension}\n"
123        net_scheme.file(machine=m, filename='/etc/hosts', content=hosts_start + hosts, permissions=0o0644,
124                        owner="root:root")

Write /etc/hosts to each machine in machine_list.

Each file starts with the standard loopback entries (127.0.0.1 localhost and 127.0.1.1 for the machine itself), followed by the lines produced by hosts_file_content() for the machines in included.

Arguments:
  • net_scheme: the NetScheme0 instance
  • domain_extension: domain suffix appended to every hostname (e.g. 'example.com')
  • machine_list: machines that receive the /etc/hosts file; defaults to get_visibles_machines()
  • included: machines whose entries appear in the hosts table; passed through to hosts_file_content() — defaults to get_visibles_machines() when None
  • ips: dict {machine_name: [IPv4Interface|IPv4Address, ...]} — see hosts_file_content()
  • separator: string placed between fields (default: two tabs)
def change_password( net_scheme: SRE.lib_sre.NetScheme0, machine: str, username: str, password: str):
127def change_password(net_scheme: NetScheme0, machine: str, username: str, password: str):
128    """Set *username*'s password on *machine* via chpasswd.
129
130    The password is written to a temporary file (never passed on the command line).
131    """
132    # Write "username:password" to a file so the password never appears in a shell command.
133    net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd',
134                    content=f'{username}:{password}\n', permissions=0o600)
135    net_scheme.cmd(machine, 'sh -c "chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd"')

Set username's password on machine via chpasswd.

The password is written to a temporary file (never passed on the command line).

def create_user( net_scheme: SRE.lib_sre.NetScheme0, machine: str, username: str, password: str, uid: int = None, gid: int = None, shell: str = '/bin/bash'):
138def create_user(net_scheme: NetScheme0, machine: str, username: str, password: str, uid: int = None, gid: int = None, shell: str = "/bin/bash"):
139    """Create *username* on *machine* (if not already present) and set its password.
140
141    Uses ``useradd -m`` with optional *uid*/*gid* and login *shell*.  The password is written to a
142    temporary file; the username is passed via an environment variable to prevent shell injection.
143    """
144    # Write "username:password" to a file so the password never appears in a shell command.
145    net_scheme.file(machine=machine, filename='/tmp/.sre_chpasswd',
146                    content=f'{username}:{password}\n', permissions=0o600)
147    useradd_opts = f" -s {shlex.quote(shell)}"
148    if uid is not None:
149        useradd_opts += f" -u {int(uid)}"
150    if gid is not None:
151        useradd_opts += f" -g {int(gid)}"
152    # Pass the username via an env var so no user-controlled text appears inside the sh -c string.
153    # "$SRE_USER" is double-quoted in the shell command to prevent word-splitting and glob expansion.
154    net_scheme.cmd(machine,
155                   f"env SRE_USER={shlex.quote(username)} "
156                   f"sh -c 'id \"$SRE_USER\" >/dev/null 2>&1"
157                   f" || useradd{useradd_opts} -m -k /etc/skel \"$SRE_USER\";"
158                   f" chpasswd < /tmp/.sre_chpasswd; rm -f /tmp/.sre_chpasswd'")

Create username on machine (if not already present) and set its password.

Uses useradd -m with optional uid/gid and login shell. The password is written to a temporary file; the username is passed via an environment variable to prevent shell injection.

def setup_simple_tcp_server( net_scheme: SRE.lib_sre.NetScheme0, machine: str, port: int, answer: str, ip: str | ipaddress.IPv4Interface | ipaddress.IPv4Address = None):
161def setup_simple_tcp_server(net_scheme: NetScheme0, machine: str, port: int, answer: str,
162                            ip: "str | IPv4Interface | IPv4Address" = None):
163    """Setup and (re)launch an idempotent TCP server on *machine*.
164
165    The server listens on *port* — bound to *ip* if provided (the network prefix of an
166    ``IPv4Interface`` is stripped), or to ``0.0.0.0`` otherwise. On each client connection
167    it sends *answer* (UTF-8) and closes the socket. Calling this function again for the
168    same *port* kills the previous instance before relaunching.
169    """
170    if ip is None:
171        bind_addr = "0.0.0.0"
172    elif isinstance(ip, IPv4Interface):
173        bind_addr = str(ip.ip)
174    else:
175        bind_addr = str(ip).split('/')[0]
176
177    script_path = f"/usr/local/sbin/sre_tcp_server_{port}.py"
178    answer_file = f"/var/lib/sre_tcp_server_{port}.answer"
179    log_file = f"/var/log/sre_tcp_server_{port}.log"
180    pid_file = f"/run/sre_tcp_server_{port}.pid"
181
182    net_scheme.file(machine=machine, filename=answer_file, content=answer, permissions=0o644)
183
184    # The script double-forks AND closes the stdio fds inherited from docker exec —
185    # otherwise exec_run keeps streaming and the state op hangs forever. After the
186    # second fork, fds 0/1/2 are reopened on /dev/null (stdin) and the per-port log
187    # file (stdout/stderr), so binding/startup failures land in the log. The daemon
188    # also writes its PID to a per-port file so the launcher can kill the previous
189    # instance without using `pkill -f` (which would match the launcher's own sh -c
190    # argument and kill the shell before the python3 command runs).
191    script_content = (
192        "#!/usr/bin/env python3\n"
193        "import os, socket, traceback\n"
194        "if os.fork() != 0: os._exit(0)\n"
195        "os.setsid()\n"
196        "if os.fork() != 0: os._exit(0)\n"
197        "# detach from docker exec's stdio so exec_run can return\n"
198        "for fd in (0, 1, 2):\n"
199        "    try: os.close(fd)\n"
200        "    except OSError: pass\n"
201        "os.open(os.devnull, os.O_RDONLY)  # fd 0\n"
202        f"_log = os.open({log_file!r}, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)  # fd 1\n"
203        "os.dup2(_log, 2)  # fd 2 = log\n"
204        f"with open({pid_file!r}, 'w') as _pf: _pf.write(str(os.getpid()))\n"
205        "try:\n"
206        f"    with open({answer_file!r}, 'rb') as f:\n"
207        "        answer = f.read()\n"
208        "    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n"
209        "    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n"
210        f"    s.bind(({bind_addr!r}, {int(port)}))\n"
211        "    s.listen(16)\n"
212        "    while True:\n"
213        "        conn, _ = s.accept()\n"
214        "        try:\n"
215        "            conn.sendall(answer)\n"
216        "        finally:\n"
217        "            conn.close()\n"
218        "except Exception:\n"
219        "    traceback.print_exc()\n"
220        "    os._exit(1)\n"
221    )
222    net_scheme.file(machine=machine, filename=script_path, content=script_content, permissions=0o755)
223
224    quoted_script = shlex.quote(script_path)
225    quoted_pidfile = shlex.quote(pid_file)
226    # If a previous instance left a PID file, kill it and give the kernel a moment
227    # to release the port; ignore stale PIDs. Then launch the new daemon. The script
228    # daemonizes itself, so this command returns immediately.
229    net_scheme.cmd(machine,
230                   f"sh -c '[ -f {quoted_pidfile} ] && kill $(cat {quoted_pidfile}) 2>/dev/null; "
231                   f"sleep 0.2; "
232                   f"python3 {quoted_script}'")

Setup and (re)launch an idempotent TCP server on machine.

The server listens on port — bound to ip if provided (the network prefix of an IPv4Interface is stripped), or to 0.0.0.0 otherwise. On each client connection it sends answer (UTF-8) and closes the socket. Calling this function again for the same port kills the previous instance before relaunching.