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 ADDED
@@ -0,0 +1,5 @@
1
+
2
+ from .rssh import rssh
3
+ from .expdb import expdb
4
+
5
+ __all__ = ["rssh", "expdb"]
@@ -0,0 +1,5 @@
1
+
2
+ from .clmanager import clmanager
3
+ from .dbmanager import dbmanager
4
+
5
+ __all__ = ["clmanager", "dbmanager"]
@@ -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()