]>
Commit | Line | Data |
---|---|---|
352ddc72 CH |
1 | # -*- coding: utf-8 eval: (blacken-mode 1) -*- |
2 | # SPDX-License-Identifier: GPL-2.0-or-later | |
3 | # | |
4 | # December 5 2021, Christian Hopps <chopps@labn.net> | |
5 | # | |
6 | # Copyright 2021, LabN Consulting, L.L.C. | |
7 | # | |
8 | """A command that allows external command execution inside nodes.""" | |
9 | import argparse | |
10 | import json | |
11 | import os | |
12 | import subprocess | |
13 | import sys | |
14 | ||
15 | from pathlib import Path | |
16 | ||
17 | ||
18 | def newest_file_in(filename, paths, has_sibling=None): | |
19 | new = None | |
20 | newst = None | |
21 | items = (x for y in paths for x in Path(y).rglob(filename)) | |
22 | for e in items: | |
23 | st = os.stat(e) | |
24 | if has_sibling and not e.parent.joinpath(has_sibling).exists(): | |
25 | continue | |
26 | if not new or st.st_mtime_ns > newst.st_mtime_ns: | |
27 | new = e | |
28 | newst = st | |
29 | continue | |
30 | return new, newst | |
31 | ||
32 | ||
33 | def main(*args): | |
34 | ap = argparse.ArgumentParser(args) | |
35 | ap.add_argument("-d", "--rundir", help="runtime directory for tempfiles, logs, etc") | |
36 | ap.add_argument("node", nargs="?", help="node to enter or run command inside") | |
37 | ap.add_argument( | |
38 | "shellcmd", | |
39 | nargs=argparse.REMAINDER, | |
40 | help="optional shell-command to execute on NODE", | |
41 | ) | |
42 | args = ap.parse_args() | |
43 | if args.rundir: | |
44 | configpath = Path(args.rundir).joinpath("config.json") | |
45 | else: | |
46 | configpath, _ = newest_file_in( | |
47 | "config.json", | |
48 | ["/tmp/munet", "/tmp/mutest", "/tmp/unet-test"], | |
49 | has_sibling=args.node, | |
50 | ) | |
51 | print(f'Using "{configpath}"') | |
52 | ||
53 | if not configpath.exists(): | |
54 | print(f'"{configpath}" not found') | |
55 | return 1 | |
56 | rundir = configpath.parent | |
57 | ||
58 | nodes = [] | |
59 | config = json.load(open(configpath, encoding="utf-8")) | |
60 | nodes = list(config.get("topology", {}).get("nodes", [])) | |
61 | envcfg = config.get("mucmd", {}).get("env", {}) | |
62 | ||
63 | # If args.node is not a node it's part of shellcmd | |
64 | if args.node and args.node not in nodes: | |
65 | if args.node != ".": | |
66 | args.shellcmd[0:0] = [args.node] | |
67 | args.node = None | |
68 | ||
69 | if args.node: | |
70 | name = args.node | |
71 | nodedir = rundir.joinpath(name) | |
72 | if not nodedir.exists(): | |
73 | print('"{name}" node doesn\'t exist in "{rundir}"') | |
74 | return 1 | |
75 | rundir = nodedir | |
76 | else: | |
77 | name = "munet" | |
78 | pidpath = rundir.joinpath("nspid") | |
79 | pid = open(pidpath, encoding="ascii").read().strip() | |
80 | ||
81 | env = {**os.environ} | |
82 | env["MUNET_NODENAME"] = name | |
83 | env["MUNET_RUNDIR"] = str(rundir) | |
84 | ||
85 | for k in envcfg: | |
86 | envcfg[k] = envcfg[k].replace("%NAME%", str(name)) | |
87 | envcfg[k] = envcfg[k].replace("%RUNDIR%", str(rundir)) | |
88 | ||
89 | # Can't use -F if it's a new pid namespace | |
90 | ecmd = "/usr/bin/nsenter" | |
91 | eargs = [ecmd] | |
92 | ||
93 | output = subprocess.check_output(["/usr/bin/nsenter", "--help"], encoding="utf-8") | |
94 | if " -a," in output: | |
95 | eargs.append("-a") | |
96 | else: | |
97 | # -U doesn't work | |
98 | for flag in ["-u", "-i", "-m", "-n", "-C", "-T"]: | |
99 | if f" {flag}," in output: | |
100 | eargs.append(flag) | |
101 | eargs.append(f"--pid=/proc/{pid}/ns/pid_for_children") | |
102 | eargs.append(f"--wd={rundir}") | |
103 | eargs.extend(["-t", pid]) | |
104 | eargs += args.shellcmd | |
105 | # print("Using ", eargs) | |
106 | return os.execvpe(ecmd, eargs, {**env, **envcfg}) | |
107 | ||
108 | ||
109 | if __name__ == "__main__": | |
110 | exit_status = main() | |
111 | sys.exit(exit_status) |