badwulf 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- badwulf/__init__.py +5 -0
- badwulf/cli/__init__.py +5 -0
- badwulf/cli/clmanager.py +298 -0
- badwulf/cli/dbmanager.py +453 -0
- badwulf/expdb.py +832 -0
- badwulf/rssh.py +255 -0
- badwulf/tools.py +306 -0
- badwulf-0.1.0.dist-info/LICENSE +181 -0
- badwulf-0.1.0.dist-info/METADATA +39 -0
- badwulf-0.1.0.dist-info/RECORD +12 -0
- badwulf-0.1.0.dist-info/WHEEL +5 -0
- badwulf-0.1.0.dist-info/top_level.txt +1 -0
badwulf/__init__.py
ADDED
badwulf/cli/__init__.py
ADDED
badwulf/cli/clmanager.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
import platform
|
|
4
|
+
import subprocess
|
|
5
|
+
import argparse
|
|
6
|
+
import datetime
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
from time import sleep
|
|
9
|
+
|
|
10
|
+
from ..tools import *
|
|
11
|
+
from ..rssh import rssh
|
|
12
|
+
|
|
13
|
+
class clmanager:
|
|
14
|
+
"""
|
|
15
|
+
Command line utility for Beowulf clusters
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
name,
|
|
20
|
+
nodes,
|
|
21
|
+
version,
|
|
22
|
+
date,
|
|
23
|
+
description,
|
|
24
|
+
readme = None,
|
|
25
|
+
program = None,
|
|
26
|
+
username = None,
|
|
27
|
+
server = None,
|
|
28
|
+
server_username = False,
|
|
29
|
+
port = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize a cluster CLI utility program
|
|
32
|
+
:param name: The name of the cluster/server
|
|
33
|
+
:param nodes: A list of nodenames or dict in the form {alias: nodename}
|
|
34
|
+
:param version: The version of the program
|
|
35
|
+
:param date: The date of the program's last revision
|
|
36
|
+
:param description: A description of the program
|
|
37
|
+
:param readme: The file path of a README.md file
|
|
38
|
+
:param program: The name of the program (defaults to name)
|
|
39
|
+
:param username: Your username on the cluster
|
|
40
|
+
:param server: The gateway server hostname (optional)
|
|
41
|
+
:param server_username: Your username on the gateway server (optional)
|
|
42
|
+
:param port: The local port for gateway server SSH forwarding
|
|
43
|
+
"""
|
|
44
|
+
self.name = name
|
|
45
|
+
self.nodes = nodes
|
|
46
|
+
self.version = version
|
|
47
|
+
if isinstance(date, datetime.date):
|
|
48
|
+
self.date = date
|
|
49
|
+
else:
|
|
50
|
+
self.date = datetime.date.fromisoformat(date)
|
|
51
|
+
self.description = description
|
|
52
|
+
self.readme = readme
|
|
53
|
+
if program is None:
|
|
54
|
+
self.program = name.casefold()
|
|
55
|
+
else:
|
|
56
|
+
self.program = program
|
|
57
|
+
self.username = username
|
|
58
|
+
self.server = server
|
|
59
|
+
self.server_username = server_username
|
|
60
|
+
self.port = port
|
|
61
|
+
self._parser = None
|
|
62
|
+
self._args = None
|
|
63
|
+
|
|
64
|
+
def _add_cluster_args(self, parser):
|
|
65
|
+
"""
|
|
66
|
+
Add cluster parameters to a parser.
|
|
67
|
+
:param parser: The parser to update
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(self.nodes, dict):
|
|
70
|
+
for alias, nodename in self.nodes.items():
|
|
71
|
+
parser.add_argument(f"-{alias}", action="append_const",
|
|
72
|
+
help=nodename, dest="nodes", const=nodename)
|
|
73
|
+
parser.add_argument("-n", "--node", action="append",
|
|
74
|
+
help=f"{self.name} node", dest="nodes",
|
|
75
|
+
metavar="NODE")
|
|
76
|
+
parser.add_argument("-p", "--port", action="store",
|
|
77
|
+
help="port forwarding", default=self.port)
|
|
78
|
+
parser.add_argument("-u", "--user", action="store",
|
|
79
|
+
help=f"{self.name} user", default=self.username)
|
|
80
|
+
parser.add_argument("-L", "--login", action="store",
|
|
81
|
+
help="gateway server user", default=self.server_username)
|
|
82
|
+
parser.add_argument("-S", "--server", action="store",
|
|
83
|
+
help="gateway server host", default=self.server)
|
|
84
|
+
|
|
85
|
+
def _add_subcommand_run(self, subparsers):
|
|
86
|
+
"""
|
|
87
|
+
Add 'run' subcommand to subparsers.
|
|
88
|
+
:param subparsers: The subparsers to update
|
|
89
|
+
"""
|
|
90
|
+
cmd = subparsers.add_parser("run",
|
|
91
|
+
help=f"run command (e.g., shell) on a {self.name} node")
|
|
92
|
+
self._add_cluster_args(cmd)
|
|
93
|
+
cmd.add_argument("remote_command", action="store",
|
|
94
|
+
help="command to execute on a Magi node", nargs=argparse.OPTIONAL,
|
|
95
|
+
metavar="command")
|
|
96
|
+
cmd.add_argument("remote_args", action="store",
|
|
97
|
+
help="command arguments", nargs=argparse.REMAINDER,
|
|
98
|
+
metavar="...")
|
|
99
|
+
|
|
100
|
+
def _add_subcommand_copy_id(self, subparsers):
|
|
101
|
+
"""
|
|
102
|
+
Add 'copy-id' subcommand to subparsers.
|
|
103
|
+
:param subparsers: The subparsers to update
|
|
104
|
+
"""
|
|
105
|
+
cmd = subparsers.add_parser("copy-id",
|
|
106
|
+
help=f"copy ssh keys to a {self.name} node")
|
|
107
|
+
self._add_cluster_args(cmd)
|
|
108
|
+
cmd.add_argument("identity_file", action="store",
|
|
109
|
+
help="ssh key identity file")
|
|
110
|
+
|
|
111
|
+
def _add_subcommand_push(self, subparsers):
|
|
112
|
+
"""
|
|
113
|
+
Add a 'push' subcommand to subparsers.
|
|
114
|
+
:param subparsers: The subparsers to update
|
|
115
|
+
"""
|
|
116
|
+
cmd = subparsers.add_parser("push",
|
|
117
|
+
help=f"upload file(s) to {self.name}")
|
|
118
|
+
self._add_cluster_args(cmd)
|
|
119
|
+
cmd.add_argument("src", action="store",
|
|
120
|
+
help="source file/directory")
|
|
121
|
+
cmd.add_argument("dest", action="store",
|
|
122
|
+
help="destination file/directory")
|
|
123
|
+
cmd.add_argument("--ask", action="store_true",
|
|
124
|
+
help="ask to confirm before uploading files?")
|
|
125
|
+
cmd.add_argument("--dry-run", action="store_true",
|
|
126
|
+
help="show what would happen without doing it?")
|
|
127
|
+
|
|
128
|
+
def _add_subcommand_pull(self, subparsers):
|
|
129
|
+
"""
|
|
130
|
+
Add a 'pull' subcommand to subparsers.
|
|
131
|
+
:param subparsers: The subparsers to update
|
|
132
|
+
"""
|
|
133
|
+
cmd = subparsers.add_parser("pull",
|
|
134
|
+
help=f"download file(s) from {self.name}")
|
|
135
|
+
self._add_cluster_args(cmd)
|
|
136
|
+
cmd.add_argument("src", action="store",
|
|
137
|
+
help="source file/directory")
|
|
138
|
+
cmd.add_argument("dest", action="store",
|
|
139
|
+
help="destination file/directory")
|
|
140
|
+
cmd.add_argument("--ask", action="store_true",
|
|
141
|
+
help="ask to confirm before downloading files?")
|
|
142
|
+
cmd.add_argument("--dry-run", action="store_true",
|
|
143
|
+
help="show what would happen without doing it?")
|
|
144
|
+
|
|
145
|
+
def _add_subcommand_readme(self, subparsers):
|
|
146
|
+
"""
|
|
147
|
+
Add 'readme' subcommand to subparsers.
|
|
148
|
+
:param subparsers: The subparsers to update
|
|
149
|
+
"""
|
|
150
|
+
cmd = subparsers.add_parser("readme",
|
|
151
|
+
help="display readme")
|
|
152
|
+
cmd.add_argument("-p", "--pager", action="store",
|
|
153
|
+
help="program to display readme (default 'glow')")
|
|
154
|
+
cmd.add_argument("-w", "--width", action="store",
|
|
155
|
+
help="word-wrap readme at width (default 70)", default=70)
|
|
156
|
+
|
|
157
|
+
def _init_parser(self):
|
|
158
|
+
"""
|
|
159
|
+
Initialize the argument parser
|
|
160
|
+
"""
|
|
161
|
+
parser = argparse.ArgumentParser(self.program,
|
|
162
|
+
description=self.description)
|
|
163
|
+
parser.add_argument("-v", "--version", action="store_true",
|
|
164
|
+
help="display version")
|
|
165
|
+
subparsers = parser.add_subparsers(dest="cmd")
|
|
166
|
+
self._add_subcommand_run(subparsers)
|
|
167
|
+
self._add_subcommand_copy_id(subparsers)
|
|
168
|
+
self._add_subcommand_push(subparsers)
|
|
169
|
+
self._add_subcommand_pull(subparsers)
|
|
170
|
+
if self.readme is not None:
|
|
171
|
+
self._add_subcommand_readme(subparsers)
|
|
172
|
+
self._parser = parser
|
|
173
|
+
|
|
174
|
+
def is_node(self):
|
|
175
|
+
"""
|
|
176
|
+
Check if the program is running on a cluster node
|
|
177
|
+
:returns: True if running the a cluster node, False otherwise
|
|
178
|
+
"""
|
|
179
|
+
if isinstance(self.nodes, dict):
|
|
180
|
+
nodes = self.nodes.values()
|
|
181
|
+
else:
|
|
182
|
+
nodes = self.nodes
|
|
183
|
+
return is_known_host(nodes)
|
|
184
|
+
|
|
185
|
+
def resolve_node(self, nodes):
|
|
186
|
+
"""
|
|
187
|
+
Get single valid nodename from a list of nodes
|
|
188
|
+
:param nodes: A list of nodenames
|
|
189
|
+
"""
|
|
190
|
+
host = platform.node().replace(".local", "")
|
|
191
|
+
if nodes is None or len(nodes) != 1:
|
|
192
|
+
sys.exit(f"{self.program}: error: must specify exactly _one_ {self.name} node")
|
|
193
|
+
node = nodes[0]
|
|
194
|
+
if self.is_node():
|
|
195
|
+
if host == node.casefold():
|
|
196
|
+
node = "localhost"
|
|
197
|
+
else:
|
|
198
|
+
node += ".local"
|
|
199
|
+
return node
|
|
200
|
+
|
|
201
|
+
def open_ssh(self,
|
|
202
|
+
node,
|
|
203
|
+
username = None,
|
|
204
|
+
server = None,
|
|
205
|
+
server_username = None,
|
|
206
|
+
port = None):
|
|
207
|
+
"""
|
|
208
|
+
Open SSH connection to a cluster node
|
|
209
|
+
:param node: The target cluster node
|
|
210
|
+
:param username: Your username on the cluster
|
|
211
|
+
:param server: The gateway server hostname (optional)
|
|
212
|
+
:param server_username: Your username on the gateway server (optional)
|
|
213
|
+
:param port: Port used for gateway forwarding
|
|
214
|
+
:returns: An open rssh instance
|
|
215
|
+
"""
|
|
216
|
+
if username is None:
|
|
217
|
+
username = self.username
|
|
218
|
+
if server is None:
|
|
219
|
+
server = self.server
|
|
220
|
+
if server_username is None:
|
|
221
|
+
server_username = self.server_username
|
|
222
|
+
if port is None:
|
|
223
|
+
port = findport()
|
|
224
|
+
# connect and return the session
|
|
225
|
+
session = rssh(username, node,
|
|
226
|
+
server=server,
|
|
227
|
+
server_username=server_username,
|
|
228
|
+
port=port,
|
|
229
|
+
autoconnect=True)
|
|
230
|
+
return session
|
|
231
|
+
|
|
232
|
+
def parse_args(self):
|
|
233
|
+
"""
|
|
234
|
+
Parse command line arguments
|
|
235
|
+
"""
|
|
236
|
+
if self._parser is None:
|
|
237
|
+
self._init_parser()
|
|
238
|
+
self._args = self._parser.parse_args()
|
|
239
|
+
|
|
240
|
+
def main(self):
|
|
241
|
+
"""
|
|
242
|
+
Run the program
|
|
243
|
+
"""
|
|
244
|
+
if self._args is None:
|
|
245
|
+
self.parse_args()
|
|
246
|
+
args = self._args
|
|
247
|
+
# version
|
|
248
|
+
if args.version:
|
|
249
|
+
description = self.description.splitlines()[0]
|
|
250
|
+
print(f"{description} version {self.version} (revised {self.date})")
|
|
251
|
+
print(badwulf_attribution())
|
|
252
|
+
sys.exit()
|
|
253
|
+
# open ssh for server commands
|
|
254
|
+
if args.cmd in ("run", "copy-id", "push", "pull"):
|
|
255
|
+
con = self.open_ssh(self.resolve_node(args.nodes),
|
|
256
|
+
username=args.user,
|
|
257
|
+
server=args.server,
|
|
258
|
+
server_username=args.login,
|
|
259
|
+
port=args.port)
|
|
260
|
+
sleep(1) # allow time to connect
|
|
261
|
+
# help
|
|
262
|
+
if args.cmd is None:
|
|
263
|
+
self._parser.print_help()
|
|
264
|
+
# run
|
|
265
|
+
elif args.cmd == "run":
|
|
266
|
+
if args.remote_command is None:
|
|
267
|
+
con.ssh()
|
|
268
|
+
else:
|
|
269
|
+
print(f"connecting as {con.username}@{con.destination}")
|
|
270
|
+
dest = f"{con.username}@{con.hostname}"
|
|
271
|
+
if con.server is None:
|
|
272
|
+
cmd = ["ssh", dest]
|
|
273
|
+
else:
|
|
274
|
+
cmd = ["ssh", "-o", "NoHostAuthenticationForLocalhost=yes"]
|
|
275
|
+
cmd += ["-p", str(con.port), dest]
|
|
276
|
+
cmd.append(args.remote_command)
|
|
277
|
+
cmd.extend(args.remote_args)
|
|
278
|
+
subprocess.run(cmd)
|
|
279
|
+
# copy-id
|
|
280
|
+
elif args.cmd == "copy-id":
|
|
281
|
+
con.copy_id(args.identity_file)
|
|
282
|
+
# push
|
|
283
|
+
elif args.cmd == "push":
|
|
284
|
+
con.upload(args.src, args.dest,
|
|
285
|
+
dry_run=args.dry_run, ask=args.ask)
|
|
286
|
+
# pull
|
|
287
|
+
elif args.cmd == "pull":
|
|
288
|
+
con.download(args.src, args.dest,
|
|
289
|
+
dry_run=args.dry_run, ask=args.ask)
|
|
290
|
+
# readme
|
|
291
|
+
elif args.cmd == "readme":
|
|
292
|
+
if args.pager is None:
|
|
293
|
+
cmd = ["glow", "-p", "-w", str(args.width)]
|
|
294
|
+
else:
|
|
295
|
+
cmd = [args.pager]
|
|
296
|
+
cmd += [self.readme]
|
|
297
|
+
subprocess.run(cmd)
|
|
298
|
+
sys.exit()
|