patme 0.4.4__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.

Potentially problematic release.


This version of patme might be problematic. Click here for more details.

Files changed (46) hide show
  1. patme/__init__.py +52 -0
  2. patme/buildtools/__init__.py +7 -0
  3. patme/buildtools/rce_releasecreator.py +336 -0
  4. patme/buildtools/release.py +26 -0
  5. patme/femtools/__init__.py +5 -0
  6. patme/femtools/abqmsgfilechecker.py +137 -0
  7. patme/femtools/fecall.py +1092 -0
  8. patme/geometry/__init__.py +0 -0
  9. patme/geometry/area.py +124 -0
  10. patme/geometry/coordinatesystem.py +635 -0
  11. patme/geometry/intersect.py +284 -0
  12. patme/geometry/line.py +183 -0
  13. patme/geometry/misc.py +420 -0
  14. patme/geometry/plane.py +464 -0
  15. patme/geometry/rotate.py +244 -0
  16. patme/geometry/scale.py +152 -0
  17. patme/geometry/shape2d.py +50 -0
  18. patme/geometry/transformations.py +1831 -0
  19. patme/geometry/translate.py +139 -0
  20. patme/mechanics/__init__.py +4 -0
  21. patme/mechanics/loads.py +435 -0
  22. patme/mechanics/material.py +1260 -0
  23. patme/service/__init__.py +7 -0
  24. patme/service/decorators.py +85 -0
  25. patme/service/duration.py +96 -0
  26. patme/service/exceptionhook.py +104 -0
  27. patme/service/exceptions.py +36 -0
  28. patme/service/io/__init__.py +3 -0
  29. patme/service/io/basewriter.py +122 -0
  30. patme/service/logger.py +375 -0
  31. patme/service/mathutils.py +108 -0
  32. patme/service/misc.py +71 -0
  33. patme/service/moveimports.py +217 -0
  34. patme/service/stringutils.py +419 -0
  35. patme/service/systemutils.py +290 -0
  36. patme/sshtools/__init__.py +3 -0
  37. patme/sshtools/cara.py +435 -0
  38. patme/sshtools/clustercaller.py +420 -0
  39. patme/sshtools/facluster.py +350 -0
  40. patme/sshtools/sshcall.py +168 -0
  41. patme-0.4.4.dist-info/LICENSE +21 -0
  42. patme-0.4.4.dist-info/LICENSES/MIT.txt +9 -0
  43. patme-0.4.4.dist-info/METADATA +168 -0
  44. patme-0.4.4.dist-info/RECORD +46 -0
  45. patme-0.4.4.dist-info/WHEEL +4 -0
  46. patme-0.4.4.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,350 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """
6
+ Connect via python to the FA cluster using ssh, submit commands, move inputs/outputs to and from the cluster.
7
+
8
+ **Scenario**
9
+
10
+ To connect to the institute cluster and submit a job, two things need to be done.
11
+ First the input files must be copied to the cluster.
12
+
13
+ ``\\\\cluster.fa.bs.dlr.de\\<username>\\``
14
+
15
+ Secondly, when all required files are available on the cluster,
16
+ the cluster command (see cluster documentation) needs to be sent using a secure connection.
17
+ This is done via ssh using the rsa-public/privat-key algorithm.
18
+
19
+ **Connect for the first time**
20
+
21
+ - Create a public+private key using ssh-keygen
22
+ - Put the private key to the location specified in patme.sshtools.sshcall.privateKeyFileConfig
23
+ or adapt this variable at runtime.
24
+ - Append the public key to "~/.ssh/authorized_keys" on the remote computer
25
+
26
+ **HowTo connect to the a host**
27
+
28
+ >> sshCluster('echo hello world')
29
+ 'hello world\\n'
30
+
31
+ """
32
+
33
+ from time import sleep, time
34
+
35
+ from patme.service.exceptions import DelisSshError
36
+ from patme.service.logger import log
37
+ from patme.sshtools import sshcall
38
+
39
+
40
+ def _getClusterAuthentication():
41
+ """Returns the hostname, host key string and private key file information for a cluster call.
42
+
43
+ A description to these objects can be found in service.sshremotecall.callSSH"""
44
+ hostname = "cluster.fa.bs.dlr.de"
45
+ # hostname = '129.247.54.37' # in vpn, the host name is not reachable
46
+ bsfalxclusterKeyString = "AAAAB3NzaC1yc2EAAAADAQABAAABAQDIibHwDzn+UQFI3PRve7YSUdj72lBAG4ARvzrbXNOZJ04/nFMlS5vee8AB+EWOqMj56xa4fWX2BOcBfpI69dp0oDMWuotqLKUpRIQoSlYeE7wZ34KXr2F3nEZ9Zm8Y0OWCGVPgO3euLiBz6Rt+veuNN2A/2EyBM36Ij8T9czNHzOqjR6lpkqPVDC2Rdfjiytvy+iMYnGR1wTNqjPlfLHz4YTWM4dF/H8pd1SQ0maNKXCH9Fr4zPV6uxMJURzm+wjPC+ccG/On3jCsGxxjyVQ3PnHwPrK0Pds0yFZ00Kf/y9lpHaP1yjD955NB7cINhCZKenTl8gdYZ6u7Qub+OV2kJ"
47
+ """this is the key from the file "~/.ssh/known_hosts" used by openssh on a linux machine"""
48
+ sshcall.checkPrivatePublicKey()
49
+ return hostname, bsfalxclusterKeyString, sshcall.privateKeyFileConfig
50
+
51
+
52
+ def sshClusterJob(
53
+ remoteCommand, printOutput=True, checkInterval=5, time2WaitForJob=30, monitorNodeUsage=False, **kwargs
54
+ ):
55
+ """Submit a job to the institute cluster via ssh and return when terminated.
56
+
57
+ After job submission, a connection to the cluster is established every 'checkInterval' seconds
58
+ to check if the job's status is already set to 'COMPLETED'.
59
+
60
+ :param remoteCommand: String with command for cluster. The arguments for the queuing
61
+ system must not contain the option '-i' to wait for job
62
+ completion.
63
+ :param printOutput: True (default) will print output created by the ssh call.
64
+ :param checkInterval: Time period in seconds between job completion checks
65
+ :param time2WaitForJob: After job submission it might take some time for the cluster to
66
+ add the job to the queue. Enter max seconds [int] to wait.
67
+
68
+ :return: int, job id
69
+ """
70
+ retVal = sshCluster(remoteCommand, printOutput=printOutput)
71
+ errorMsg = lambda remoteCommand, retVal: f"remote command: {remoteCommand}\nreturn value: {retVal}"
72
+ if not retVal or (retVal and "Submitted batch job" not in retVal):
73
+ raise DelisSshError(
74
+ "Job submission to cluster failed or maybe the arguments "
75
+ + "for the cluster contained the option -i\n"
76
+ + errorMsg(remoteCommand, retVal)
77
+ )
78
+ try:
79
+ jobId = int(retVal.split()[-1])
80
+ except:
81
+ raise DelisSshError("Could not extract job id for cluster job submission.\n" + errorMsg(remoteCommand, retVal))
82
+
83
+ log.info("Job enqueued. JobId: %s" % jobId)
84
+ usageWarningDone = False
85
+ while not clusterJobEnded(jobId, time2WaitForJob):
86
+ if monitorNodeUsage:
87
+ if not usageWarningDone:
88
+ nodeName = getNodeOfJob(jobId)
89
+ usageWarningDone = printNodeUtilization(nodeName, usageWarningDone)
90
+ sleep(checkInterval)
91
+ return jobId
92
+
93
+
94
+ def sshCluster(remoteCommand, printOutput=True, **kwargs):
95
+ """Submit a job to the institute cluster via ssh.
96
+
97
+ The method does not
98
+ wait for the completion of the cluster call. Please use sshClusterJob instead.
99
+
100
+ :param remoteCommand: String with command for cluster
101
+ :param printOutput: True (default) will print output created by the ssh call.
102
+ """
103
+ hostname, bsfalxclusterKeyString, privateKeyFile = _getClusterAuthentication()
104
+ return sshcall.callSSH(
105
+ hostname, remoteCommand, privateKeyFile, None, bsfalxclusterKeyString, printOutput=printOutput
106
+ )
107
+
108
+
109
+ def clusterJobEnded(jobId, time2WaitForJob=30, printOutput=False):
110
+ """Checks if jobId is still listed in the cluster queue.
111
+
112
+ :param jobId: Id of job running on the cluster <int>
113
+ :param time2WaitForJob: After job submission it might take some time for the cluster to
114
+ add the job to the queue. Enter max seconds [int] to wait.
115
+ :param printOutput: Flag if the ssh output should be output. Defaults to True
116
+ :return: True if job with jobId still exists in the queue, else False
117
+ :raise DelisSshError: in case job is neither running nor completed successfully
118
+ """
119
+ status = clusterJobStatus(jobId, printOutput=printOutput)
120
+ if not status and time2WaitForJob:
121
+ startTime = time()
122
+ while not status:
123
+ status = clusterJobStatus(jobId, printOutput=printOutput)
124
+ if time() - startTime > time2WaitForJob:
125
+ raise DelisSshError("Could not obtain status of cluster job with id %s" % jobId)
126
+ if not status:
127
+ raise DelisSshError("Job with id %s not found in cluster job history." % jobId)
128
+ elif "PENDING" == status:
129
+ log.debug("Job execution on cluster is waiting for resources.")
130
+ return False
131
+ elif status in ["RESIZING", "RUNNING", "REQUEUED"]:
132
+ return False
133
+ elif "FAILED" == status:
134
+ log.debug("Job with id %s failed" % jobId)
135
+ return True
136
+ elif "CANCELLED" == status:
137
+ log.debug("Job with id %s was cancelled." % jobId)
138
+ return True
139
+ elif "COMPLETED" == status:
140
+ return True
141
+ else:
142
+ raise DelisSshError('Unknown cluster status: "%s"' % status)
143
+
144
+
145
+ def clusterJobStatus(jobId, printOutput=False):
146
+ """Checks if cluster process with id "jobID" is pending.
147
+
148
+ :param jobId: id of cluster process (int)
149
+ :return: True if state is pending, else False
150
+ """
151
+ sacct = sshCluster("sacct -o state -n -j " + str(jobId), printOutput=printOutput)
152
+ return sacct.split("\n")[0].replace("+", "").strip()
153
+
154
+
155
+ def copyClusterFilesSCP(files, srcBaseDir=".", destBaseDir=".", mode="put", keytype="ssh-rsa", port=None, **kwargs):
156
+ hostname, bsfalxclusterKeyString, privateKeyFile = _getClusterAuthentication()
157
+ sshcall.copyFilesSCP(
158
+ files, hostname, privateKeyFile, None, srcBaseDir, destBaseDir, bsfalxclusterKeyString, mode, keytype, port
159
+ )
160
+
161
+
162
+ def _wrapSshCluster(*args, **kwargs):
163
+ """This method wraps the sshCluster routine to prevent python cyclic imports"""
164
+ retries = 3
165
+ for retry in range(retries):
166
+ try:
167
+ result = sshCluster(*args, **kwargs)
168
+ break
169
+ except Exception as e:
170
+ if retry < retries:
171
+ log.error(f"Got an error while calling the cluster (retry in 60s): {e}")
172
+ time.sleep(60)
173
+ else:
174
+ raise
175
+ return result
176
+
177
+
178
+ def numberOfClusterJobsAvailable(exclusiveNode=False):
179
+ """Checks and returns the number of available jobs on the FA cluster.
180
+
181
+ :param exclusiveNode: if True, number of cluster jobs is given, that can allocate
182
+ a complete node. The default is False
183
+
184
+ :returns: Returns the number of jobs that can be executed on the cluster.
185
+ """
186
+ clusterCommand = 'sinfo -h -o "%t %N";'
187
+ clusterCommand += 'squeue -h -t RUNNING,COMPLETING -o "%N"'
188
+ clusterOutput = _wrapSshCluster(clusterCommand, printOutput=False).split("\n")
189
+
190
+ # STATE NODELIST <- this line does not appear in clusterOutput
191
+ # mix node[1,3] <- these nodes have one or more active jobs
192
+ # alloc node5 <- these nodes are exclusively used
193
+ # idle node[2,4,6] <- these nodes are awaiting jobs (up to 2)
194
+ # NODELIST <- this line does not appear in clusterOutput
195
+ # node5 <- job on node5
196
+ # node1
197
+ # node3
198
+ # node3
199
+
200
+ mixNodes = _splitNodes([line.split()[1][4:].strip("[]") for line in clusterOutput if "mix" in line])
201
+ idleNodes = _splitNodes([line.split()[1][4:].strip("[]") for line in clusterOutput if "idle" in line])
202
+ nodeNumbersOfActiveJobs = [int(line[4:].strip()) for line in clusterOutput if line.startswith("node")]
203
+
204
+ numberOfPosssibleJobs = 0
205
+ if exclusiveNode:
206
+ numberOfPosssibleJobs = len(idleNodes)
207
+ else:
208
+ for mixNode in mixNodes:
209
+ if nodeNumbersOfActiveJobs.count(mixNode) < 2:
210
+ numberOfPosssibleJobs += 1
211
+ numberOfPosssibleJobs += len(idleNodes) * 2
212
+ return numberOfPosssibleJobs
213
+
214
+
215
+ def _splitNodes(nodes):
216
+ """parses the nodes string and returns a list of node numbers
217
+
218
+ Example:
219
+
220
+ >>> inputString = ['1,4', '2-3,5-6']
221
+ >>> _splitNodes(inputString)
222
+ [1, 2, 3, 4, 5, 6]
223
+ """
224
+ outputNodes = []
225
+ for nodesString in nodes:
226
+ groups = nodesString.split(",")
227
+ for group in groups:
228
+ groupMembers = group.split("-")
229
+ if len(groupMembers) > 1:
230
+ outputNodes.extend(range(int(groupMembers[0]), int(groupMembers[1]) + 1))
231
+ else:
232
+ outputNodes.append(int(groupMembers[0]))
233
+ return list(set(outputNodes))
234
+
235
+
236
+ def numberOfIdleClusterNodes():
237
+ """returns the number of idle cluster nodes
238
+
239
+ cluster call returns: "3/3" which is Allocated/Idle
240
+
241
+ Attention: This is not the number of possible cluster jobs, since 2 jobs can be run
242
+ at each node. If zero nodes are idle, there may be still the opportunity to start
243
+ a job right away.
244
+ """
245
+ clusterOutput = _wrapSshCluster('sinfo -h -e -o "%A"', printOutput=False)
246
+ return int(clusterOutput.split("/")[-1])
247
+
248
+
249
+ def getNodeUtilization(nodeName="head"):
250
+ """Returns the utilization of the cluster head (default) or of one of its nodes.
251
+ The information is retrieved using the commands vmstat and df. The keys of the
252
+ returned dictionary are described in the following.
253
+
254
+ Processes
255
+ r: The number of processes waiting for run time.
256
+ b: The number of processes in uninterruptible sleep.
257
+ RAM Memory
258
+ swpd: The amount of virtual memory used. (in MB)
259
+ free: The amount of idle memory. (in MB)
260
+ buff: The amount of memory used as buffers. (in MB)
261
+ cache: The amount of memory used as cache. (in MB)
262
+ Swap Memory
263
+ si: Amount of memory swapped in from disk (in MB/s).
264
+ so: Amount of memory swapped to disk (in MB/s).
265
+ IO
266
+ bi: Blocks received from a block device (blocks/s).
267
+ bo: Blocks sent to a block device (blocks/s).
268
+ System
269
+ in: The number of interrupts per second, including the clock.
270
+ cs: The number of context switches per second.
271
+ CPU
272
+ These are percentages of total CPU time.
273
+ us: Time spent running non-kernel code. (user time, including nice time)
274
+ sy: Time spent running kernel code. (system time)
275
+ id: Time spent idle. Prior to Linux 2.5.41, this includes IO-wait time.
276
+ wa: Time spent waiting for IO. Prior to Linux 2.5.41, shown as zero.
277
+ HDD Memory
278
+ 1K-blocks: Total size of storage memory (in KB)
279
+ Used: Total size of used storage memory (in KB)
280
+ Available: Total size of available storage memory (in KB)
281
+ Use%: Relative usage of storage memory (in %)
282
+
283
+ :param nodeName: Name of the node (node1, ...) of which the information is to
284
+ be retrieved. The default is "head".
285
+ :return: Dictionary with utilization information.
286
+ """
287
+ nodeCmdString = ""
288
+ filesystem = "/home"
289
+ if nodeName != "head":
290
+ nodeCmdString = "ssh " + nodeName + " "
291
+ filesystem = "/dev/sda3"
292
+ remoteCmd = nodeCmdString + "vmstat -S M;"
293
+ remoteCmd += nodeCmdString + "df -l -k"
294
+ remoteCmdOutput = _wrapSshCluster(remoteCmd, printOutput=False).split("\n")
295
+ vmstatDict = dict(zip(remoteCmdOutput[1].split(), [float(item) for item in remoteCmdOutput[2].split()]))
296
+ dfData = [row for row in remoteCmdOutput if row.startswith(filesystem)][0]
297
+ dfDict = dict(zip(remoteCmdOutput[3].split()[1:-1], [float(item.strip("%")) for item in dfData.split()[1:-1]]))
298
+ vmstatDict.update(dfDict)
299
+ return vmstatDict
300
+
301
+
302
+ def printNodeUtilization(self, nodeName, printOnCriticalUtilization=False):
303
+ """Prints the utilization of a cluster node
304
+
305
+ :param nodeName: name of the cluster node to inspect
306
+ :param printOnCriticalUtilization: Flag if only on a critical utilization, the routine should print anything
307
+ :return: Flag if a usage warning was emit
308
+ :raise DelisSshError: if nodeName could not be found
309
+ """
310
+ if printOnCriticalUtilization:
311
+ logMethod = log.warn
312
+ else:
313
+ logMethod = log.info
314
+ usageWarningDone = False
315
+ if nodeName:
316
+ utilizationInfo = getNodeUtilization(nodeName=nodeName)
317
+ freeRam = (utilizationInfo["free"] + utilizationInfo["cache"]) / 1024
318
+ freeHdd = utilizationInfo["Available"] / 1024 / 1024
319
+ if freeHdd < 2 or not printOnCriticalUtilization:
320
+ logMethod("HDD memory utilization of node %s critical. This may cause problems." % nodeName)
321
+ usageWarningDone = True
322
+ if freeRam < 2 or not printOnCriticalUtilization:
323
+ logMethod("RAM memory utilization of node %s critical. This may cause problems." % nodeName)
324
+ usageWarningDone = True
325
+ else:
326
+ raise DelisSshError(
327
+ 'Utilization of the used cluster node %s cannot be performed: Node "" not found.' % nodeName
328
+ )
329
+ return usageWarningDone
330
+
331
+
332
+ def getNodeOfJob(jobId):
333
+ """Returns the name of the node on which the job with id "jobId" is being executed on.
334
+
335
+ :param jobId: Id of cluster process (int)
336
+ :return: Name of node ("node1", "node2", ...) or None if jobId is not found.
337
+ """
338
+ node = None
339
+ try:
340
+ node = _wrapSshCluster("squeue | grep " + str(jobId), printOutput=False).split()[7]
341
+ except:
342
+ log.warning("Node not found, because jobID not found in cluster queue")
343
+ return node
344
+
345
+
346
+ if __name__ == "__main__":
347
+ from patme.sshtools import sshcall
348
+
349
+ sshcall.privateKeyFileConfig = None
350
+ sshCluster("echo foobar")
@@ -0,0 +1,168 @@
1
+ # SPDX-FileCopyrightText: 2022 German Aerospace Center (DLR)
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ """
6
+ Connect via python to a host using ssh and submit commands or copy files via sftp.
7
+
8
+ The following methods utilize a method to perform ssh calls by a public/private key(recommended) or by username/password.
9
+ Please refer to the method descriptions for further details. An example is also given in patme.sshtools.cara
10
+ """
11
+ import getpass
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ from subprocess import PIPE, run
16
+
17
+ from patme.service.exceptions import ImproperParameterError
18
+ from patme.service.logger import log
19
+
20
+ _numberOfProtocolBannerExceptions = 0
21
+ userDir = os.path.expanduser("~")
22
+ privateKeyFileConfig = os.path.join(userDir, "id_rsa") # might be overwritten by config reader
23
+
24
+
25
+ def callSSH(
26
+ hostname,
27
+ remoteCommand,
28
+ privateKeyFile=privateKeyFileConfig,
29
+ username=None,
30
+ hostKeyString=None,
31
+ keytype="ssh-rsa",
32
+ port=None,
33
+ printOutput=True,
34
+ ):
35
+ """This method creates an ssh connection to a host and executes a command remotely.
36
+
37
+ :param hostname: Name of the host computer. Can be ip address or dns name.
38
+ :param remoteCommand: This is a string containing the command to be executed on a
39
+ linux machine. Be aware that the initial current directory is the home directory
40
+ and one should first change to the directory that is actually used.
41
+ :param username: name of the user on the host
42
+ :param privateKeyFile: Path and name of the private rsa key. This key is used as
43
+ private key for authentication. A copy of the corresponding public key
44
+ must be present on the host in "~/.ssh/authorized_keys".
45
+ A correct line in authorized_keys consists of the encryption type and the key: "ssh-rsa AAA....."
46
+ :param hostKeyString: Key string of the host computer.
47
+ Not required if the key is already in "~/.ssh/known_hosts".
48
+
49
+ This is the key from the file "~/.ssh/known_hosts" used by openssh on a linux machine and usually starts with "AAA..."
50
+ :param keytype: type of the ssh key for the hostKeyString ['ssh-rsa','ssh-dss']
51
+ :param port: port number of the host computer to connect to.
52
+ :param printOutput: Flag if the ssh output should be output. Defaults to True
53
+ :return: string, output from host
54
+ """
55
+ if hostKeyString:
56
+ _expandKnownHostsFile(hostname, hostKeyString, port, keytype)
57
+
58
+ callArgs = [shutil.which("ssh")]
59
+ if (privateKeyFile is not None) and os.path.exists(privateKeyFile):
60
+ callArgs += ["-i", privateKeyFile]
61
+
62
+ callArgs += ["-o", "BatchMode=true"]
63
+ if port:
64
+ callArgs += ["-p", str(port)]
65
+ if not username:
66
+ username = getpass.getuser()
67
+ callArgs += [f"{username}@{hostname}", remoteCommand]
68
+
69
+ log.info("Call ssh: " + " ".join(callArgs))
70
+ result = run(callArgs, stdout=PIPE, stderr=PIPE, universal_newlines=True, encoding="latin-1", errors="ignore")
71
+ rc, out, err = result.returncode, result.stdout, result.stderr
72
+
73
+ if printOutput or rc != 0:
74
+ log.info("SSH return string: \n" + out)
75
+ if err:
76
+ log.error("SSH error string:\n" + err)
77
+
78
+ return out
79
+
80
+
81
+ def copyFilesSCP(
82
+ files,
83
+ hostname,
84
+ privateKeyFile,
85
+ username=None,
86
+ srcBaseDir=".",
87
+ destBaseDir=".",
88
+ hostKeyString=None,
89
+ mode="put",
90
+ keytype="ssh-rsa",
91
+ port=None,
92
+ ):
93
+ """Method copies files from a local machine into a remote directory via a SSH tunnel
94
+
95
+ :param files: List of local files which are copied to the remote machine.
96
+ :param hostname: Name of the host computer. Can be ip address or dns name.
97
+ :param privateKeyFile: Path and name of the private rsa key. This key is used as
98
+ private key for authentication.
99
+ :param srcBaseDir: Source base directory of relative files to copy
100
+ :param destBaseDir: Destination directory
101
+ :param hostKeyString: Key string of the host computer.
102
+ Not required if the key is already in "~/.ssh/known_hosts".
103
+ This is the key from the file "~/.ssh/known_hosts" used by openssh on a linux machine and usually starts with "AAA..."
104
+ :param mode: mode to copy to or from a remote machine (put,get)
105
+ :param keytype: type of the ssh key for the hostKeyString ['ssh-rsa','ssh-dss']
106
+ :param port: port number of the host computer to connect to.
107
+ """
108
+ if hostKeyString:
109
+ _expandKnownHostsFile(hostname, hostKeyString, port, keytype)
110
+
111
+ if mode not in ["put", "get"]:
112
+ raise ImproperParameterError("wrong mode")
113
+
114
+ sendList = [f if os.path.isabs(f) else os.path.join(srcBaseDir, f) for f in files]
115
+
116
+ callArgs = [shutil.which("scp"), "-T"]
117
+ if (privateKeyFile is not None) and os.path.exists(privateKeyFile):
118
+ callArgs += ["-i", privateKeyFile]
119
+
120
+ callArgs += ["-o", "BatchMode=true"]
121
+ if port:
122
+ callArgs += ["-p", str(port)]
123
+ if not username:
124
+ username = getpass.getuser()
125
+ userAndHost = f"{username}@{hostname}:"
126
+ if mode == "put":
127
+ source = sendList
128
+ dest = userAndHost + destBaseDir
129
+ else:
130
+ source = [f'{userAndHost}{" ".join(sendList)}']
131
+ dest = destBaseDir
132
+
133
+ callArgs += source + [dest]
134
+ log.info("Call scp: \n" + " ".join(callArgs))
135
+
136
+ out = subprocess.check_output(callArgs).decode()
137
+ log.info("SCP result: " + out)
138
+
139
+ return out
140
+
141
+
142
+ def _expandKnownHostsFile(hostname, hostKeyString, port="", keytype="ssh-rsa"):
143
+ """Expands the known hosts file if neccessary"""
144
+ sshFolder = os.path.join(os.path.expanduser("~"), ".ssh")
145
+ if not os.path.exists(sshFolder):
146
+ os.makedirs(sshFolder)
147
+ knownHostsFile = os.path.join(sshFolder, "known_hosts")
148
+ fileMode = "a" if os.path.exists(knownHostsFile) else "w"
149
+
150
+ knownHostLine = f"[{hostname}]:{port}" if port else f"{hostname}"
151
+ knownHostLine += f" {keytype} {hostKeyString}"
152
+
153
+ knownHosts = []
154
+ if os.path.exists(knownHostsFile):
155
+ with open(knownHostsFile) as f:
156
+ knownHosts = f.readlines()
157
+ if any([knownHostLine in line for line in knownHosts]):
158
+ # host already in knownHosts file
159
+ return
160
+ else:
161
+ with open(knownHostsFile, fileMode) as f:
162
+ if knownHosts and not (knownHosts[-1] == "" or knownHosts[-1][-1] == "\n"):
163
+ f.write("\n")
164
+ f.write(f"{knownHostLine}\n")
165
+
166
+
167
+ if __name__ == "__main__":
168
+ pass
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Freund, Sebastian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.