sshreader 6.1.0__tar.gz → 6.2.0__tar.gz
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.
- {sshreader-6.1.0 → sshreader-6.2.0}/PKG-INFO +27 -54
- {sshreader-6.1.0 → sshreader-6.2.0}/README.md +14 -39
- sshreader-6.2.0/pyproject.toml +83 -0
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/__init__.py +4 -4
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/customtypes.py +2 -3
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/scripts/__init__.py +0 -1
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/scripts/pydsh.py +13 -17
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/ssh.py +13 -16
- {sshreader-6.1.0 → sshreader-6.2.0}/sshreader/utils.py +27 -29
- sshreader-6.2.0/tests/keys/id_rsa +27 -0
- sshreader-6.2.0/tests/keys/id_rsa.pub +1 -0
- sshreader-6.2.0/tests/test_pydsh.py +83 -0
- sshreader-6.2.0/tests/test_sshreader.py +242 -0
- sshreader-6.1.0/pyproject.toml +0 -53
- {sshreader-6.1.0 → sshreader-6.2.0}/LICENSE +0 -0
- {sshreader-6.1.0 → sshreader-6.2.0}/LICENSE.lesser +0 -0
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: sshreader
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.2.0
|
|
4
4
|
Summary: Multi-threading/processing wrapper for Paramiko
|
|
5
|
+
Author-Email: Jesse Almanrode <jesse@almanrode.com>
|
|
5
6
|
License: LGPL-3.0-or-later
|
|
6
|
-
|
|
7
|
-
Author-email: jesse@almanrode.com
|
|
8
|
-
Requires-Python: >3.8,<3.14
|
|
9
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
7
|
+
Classifier: Programming Language :: Python
|
|
10
8
|
Classifier: Intended Audience :: Developers
|
|
11
9
|
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
11
|
Classifier: Programming Language :: Python
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.9
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.10
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -18,30 +16,24 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
17
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
18
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
Requires-
|
|
24
|
-
Requires-Dist:
|
|
25
|
-
Requires-Dist:
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
Project-URL: homepage, http://sshreader.readthedocs.io/
|
|
20
|
+
Project-URL: repository, https://bitbucket.org/isaiah1112/sshreader/
|
|
21
|
+
Requires-Python: <4.0,>3.9
|
|
22
|
+
Requires-Dist: click<9,>=8
|
|
23
|
+
Requires-Dist: colorama<1.0,>=0.4
|
|
24
|
+
Requires-Dist: paramiko<4,>=3
|
|
25
|
+
Requires-Dist: progressbar2<5,>=4
|
|
26
|
+
Requires-Dist: python-hostlist<2,>=1
|
|
28
27
|
Description-Content-Type: text/markdown
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+

|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
A Python Module for multiprocessing/threading ssh connections in order to make ssh operations
|
|
32
|
+
across multiple servers parallel. It utilizes the [Paramiko](http://www.paramiko.org/) module
|
|
33
|
+
for its ssh client.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
In order to maintain the widest range of compatibility, [SSHreader][] is currently tested using the following versions of
|
|
38
|
-
Python:
|
|
39
|
-
|
|
40
|
-
* Python3.9
|
|
41
|
-
* Python3.10
|
|
42
|
-
* Python3.11
|
|
43
|
-
* Python3.12
|
|
44
|
-
* Python3.13
|
|
35
|
+
In order to maintain the widest range of compatibility, [SSHreader][] is currently tested
|
|
36
|
+
using [all supported versions of Python!](https://devguide.python.org/versions/)
|
|
45
37
|
|
|
46
38
|
## License
|
|
47
39
|
|
|
@@ -55,13 +47,16 @@ The most straightforward way to get the [SSHreader][] module working for you is:
|
|
|
55
47
|
```commandline
|
|
56
48
|
pip install sshreader
|
|
57
49
|
```
|
|
50
|
+
or, if you'd like to use a local install from the repo:
|
|
51
|
+
```commandline
|
|
52
|
+
make install
|
|
53
|
+
```
|
|
58
54
|
|
|
59
55
|
This ensures that all the requirements are met.
|
|
60
56
|
|
|
61
57
|
## Documentation
|
|
62
58
|
|
|
63
|
-
The documentation for [SSHreader][] can be found [
|
|
64
|
-
|
|
59
|
+
The documentation for [SSHreader][] can be found at [ReadTheDocs.](https://sshreader.readthedocs.io)
|
|
65
60
|
|
|
66
61
|
## Contributing
|
|
67
62
|
|
|
@@ -69,37 +64,16 @@ Comments and enhancements are very welcome.
|
|
|
69
64
|
|
|
70
65
|
Report any issues or feature requests on the [BitBucket bug
|
|
71
66
|
tracker](https://bitbucket.org/isaiah1112/sshreader/issues?status=new&status=open). Please include a minimal
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
not-working example which reproduces the bug and, if appropriate, the
|
|
68
|
+
traceback information. Please do not request features already being worked
|
|
74
69
|
towards.
|
|
75
70
|
|
|
76
71
|
Code contributions are encouraged: please feel free to [fork the
|
|
77
72
|
project](https://bitbucket.org/isaiah1112/sshreader) and submit pull requests to the develop branch.
|
|
78
73
|
|
|
79
|
-
### Development Installation
|
|
80
|
-
|
|
81
|
-
If you are wanting to work on development of [SSHreader][], first ensure [Poetry](https://python-poetry.org) is installed
|
|
82
|
-
and then:
|
|
83
|
-
|
|
84
|
-
```commandline
|
|
85
|
-
poetry install --with dev
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
To ensure all development requirements are met. This will allow you to run the unit/integration tests!
|
|
89
|
-
|
|
90
|
-
### Building Docs
|
|
91
|
-
|
|
92
|
-
If you have installed [Poetry](https://python-poetry.org)] you can build its Sphinx Documentation for [SSHreader][] simply by:
|
|
93
|
-
|
|
94
|
-
```commandline
|
|
95
|
-
make docs
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Then simply open `docs/build/html/index.html` in your browser.
|
|
99
|
-
|
|
100
74
|
## Extras
|
|
101
75
|
|
|
102
|
-
Included with sshreader is a script called `pydsh`. This works very similar to[pdsh](https://computing.llnl.gov/linux/pdsh.html)
|
|
76
|
+
Included with sshreader is a script called `pydsh`. This works very similar to [pdsh](https://computing.llnl.gov/linux/pdsh.html)
|
|
103
77
|
but uses sshreader at its core to perform ssh commands in parallel and return the results.
|
|
104
78
|
The output of `pydsh` can also be piped through the `dshbak` tool that comes with pdsh.
|
|
105
79
|
|
|
@@ -110,4 +84,3 @@ to process.
|
|
|
110
84
|
[GNU Lesser General Public License v3.0]: http://choosealicense.com/licenses/lgpl-3.0/ "LGPL v3"
|
|
111
85
|
|
|
112
86
|
[sshreader]: https://bitbucket.org/isaiah1112/sshreader "SSHreader Package"
|
|
113
|
-
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Python Module for multiprocessing/threading ssh connections in order to make ssh operations
|
|
4
|
+
across multiple servers parallel. It utilizes the [Paramiko](http://www.paramiko.org/) module
|
|
5
|
+
for its ssh client.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
In order to maintain the widest range of compatibility, [SSHreader][] is currently tested using the following versions of
|
|
9
|
-
Python:
|
|
10
|
-
|
|
11
|
-
* Python3.9
|
|
12
|
-
* Python3.10
|
|
13
|
-
* Python3.11
|
|
14
|
-
* Python3.12
|
|
15
|
-
* Python3.13
|
|
7
|
+
In order to maintain the widest range of compatibility, [SSHreader][] is currently tested
|
|
8
|
+
using [all supported versions of Python!](https://devguide.python.org/versions/)
|
|
16
9
|
|
|
17
10
|
## License
|
|
18
11
|
|
|
@@ -26,13 +19,16 @@ The most straightforward way to get the [SSHreader][] module working for you is:
|
|
|
26
19
|
```commandline
|
|
27
20
|
pip install sshreader
|
|
28
21
|
```
|
|
22
|
+
or, if you'd like to use a local install from the repo:
|
|
23
|
+
```commandline
|
|
24
|
+
make install
|
|
25
|
+
```
|
|
29
26
|
|
|
30
27
|
This ensures that all the requirements are met.
|
|
31
28
|
|
|
32
29
|
## Documentation
|
|
33
30
|
|
|
34
|
-
The documentation for [SSHreader][] can be found [
|
|
35
|
-
|
|
31
|
+
The documentation for [SSHreader][] can be found at [ReadTheDocs.](https://sshreader.readthedocs.io)
|
|
36
32
|
|
|
37
33
|
## Contributing
|
|
38
34
|
|
|
@@ -40,37 +36,16 @@ Comments and enhancements are very welcome.
|
|
|
40
36
|
|
|
41
37
|
Report any issues or feature requests on the [BitBucket bug
|
|
42
38
|
tracker](https://bitbucket.org/isaiah1112/sshreader/issues?status=new&status=open). Please include a minimal
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
not-working example which reproduces the bug and, if appropriate, the
|
|
40
|
+
traceback information. Please do not request features already being worked
|
|
45
41
|
towards.
|
|
46
42
|
|
|
47
43
|
Code contributions are encouraged: please feel free to [fork the
|
|
48
44
|
project](https://bitbucket.org/isaiah1112/sshreader) and submit pull requests to the develop branch.
|
|
49
45
|
|
|
50
|
-
### Development Installation
|
|
51
|
-
|
|
52
|
-
If you are wanting to work on development of [SSHreader][], first ensure [Poetry](https://python-poetry.org) is installed
|
|
53
|
-
and then:
|
|
54
|
-
|
|
55
|
-
```commandline
|
|
56
|
-
poetry install --with dev
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
To ensure all development requirements are met. This will allow you to run the unit/integration tests!
|
|
60
|
-
|
|
61
|
-
### Building Docs
|
|
62
|
-
|
|
63
|
-
If you have installed [Poetry](https://python-poetry.org)] you can build its Sphinx Documentation for [SSHreader][] simply by:
|
|
64
|
-
|
|
65
|
-
```commandline
|
|
66
|
-
make docs
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Then simply open `docs/build/html/index.html` in your browser.
|
|
70
|
-
|
|
71
46
|
## Extras
|
|
72
47
|
|
|
73
|
-
Included with sshreader is a script called `pydsh`. This works very similar to[pdsh](https://computing.llnl.gov/linux/pdsh.html)
|
|
48
|
+
Included with sshreader is a script called `pydsh`. This works very similar to [pdsh](https://computing.llnl.gov/linux/pdsh.html)
|
|
74
49
|
but uses sshreader at its core to perform ssh commands in parallel and return the results.
|
|
75
50
|
The output of `pydsh` can also be piped through the `dshbak` tool that comes with pdsh.
|
|
76
51
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
authors = [
|
|
3
|
+
{ name = "Jesse Almanrode", email = "jesse@almanrode.com" },
|
|
4
|
+
]
|
|
5
|
+
requires-python = "<4.0,>3.9"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"click<9,>=8",
|
|
8
|
+
"colorama<1.0,>=0.4",
|
|
9
|
+
"paramiko<4,>=3",
|
|
10
|
+
"progressbar2<5,>=4",
|
|
11
|
+
"python-hostlist<2,>=1",
|
|
12
|
+
]
|
|
13
|
+
name = "sshreader"
|
|
14
|
+
version = "6.2.0"
|
|
15
|
+
description = "Multi-threading/processing wrapper for Paramiko"
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
|
21
|
+
"Development Status :: 5 - Production/Stable",
|
|
22
|
+
"Programming Language :: Python",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.license]
|
|
33
|
+
text = "LGPL-3.0-or-later"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
homepage = "http://sshreader.readthedocs.io/"
|
|
37
|
+
repository = "https://bitbucket.org/isaiah1112/sshreader/"
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
pydsh = "sshreader.scripts.pydsh:cli"
|
|
41
|
+
|
|
42
|
+
[tool.pdm.build]
|
|
43
|
+
includes = [
|
|
44
|
+
"sshreader",
|
|
45
|
+
"sshreader/scripts",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = [
|
|
50
|
+
"E",
|
|
51
|
+
"F",
|
|
52
|
+
"B",
|
|
53
|
+
"UP",
|
|
54
|
+
"B",
|
|
55
|
+
"SIM",
|
|
56
|
+
"I",
|
|
57
|
+
]
|
|
58
|
+
ignore = [
|
|
59
|
+
"E501",
|
|
60
|
+
"F401",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[[tool.uv.index]]
|
|
64
|
+
name = "test-pypi"
|
|
65
|
+
url = "https://test.pypi.org/simple/"
|
|
66
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
67
|
+
explicit = true
|
|
68
|
+
|
|
69
|
+
[build-system]
|
|
70
|
+
requires = [
|
|
71
|
+
"pdm-backend",
|
|
72
|
+
]
|
|
73
|
+
build-backend = "pdm.backend"
|
|
74
|
+
|
|
75
|
+
[dependency-groups]
|
|
76
|
+
test = [
|
|
77
|
+
"coverage<8,>=7",
|
|
78
|
+
"discover<1.0,>=0.4",
|
|
79
|
+
"ruff<1.0.0,>=0.9.9",
|
|
80
|
+
]
|
|
81
|
+
docs = [
|
|
82
|
+
"sphinx<8,>=7",
|
|
83
|
+
]
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding=utf-8
|
|
2
1
|
"""A Python Package for parallelizing ssh connections via threading and multiprocessing.
|
|
3
2
|
"""
|
|
4
3
|
# Copyright (C) 2015-2025 Jesse Almanrode
|
|
@@ -17,11 +16,12 @@
|
|
|
17
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
17
|
|
|
19
18
|
# For backwards compatibility
|
|
20
|
-
from .ssh import SSH, envvars
|
|
21
|
-
from .customtypes import Command, EnvVars, Timeout, TimeoutTuple
|
|
22
|
-
from .utils import ServerJob, Hook, sshread, echo, cpu_limit, shell_command
|
|
23
19
|
import logging
|
|
24
20
|
|
|
21
|
+
from .customtypes import Command, EnvVars, Timeout, TimeoutTuple
|
|
22
|
+
from .ssh import SSH, envvars
|
|
23
|
+
from .utils import Hook, ServerJob, cpu_limit, echo, shell_command, sshread
|
|
24
|
+
|
|
25
25
|
__author__ = 'Jesse Almanrode (jesse@almanrode.com)'
|
|
26
26
|
__all__ = ['ssh', 'customtypes', 'utils']
|
|
27
27
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding=utf-8
|
|
2
1
|
""" Typing module for sshreader
|
|
3
2
|
"""
|
|
4
3
|
# Copyright (C) 2015-2025 Jesse Almanrode
|
|
@@ -15,7 +14,7 @@
|
|
|
15
14
|
#
|
|
16
15
|
# You should have received a copy of the GNU Lesser General Public License
|
|
17
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
-
from typing import NamedTuple, Optional, Union
|
|
17
|
+
from typing import NamedTuple, Optional, Union
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
class Command(NamedTuple):
|
|
@@ -34,4 +33,4 @@ class EnvVars(NamedTuple):
|
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
Timeout = Union[int, float]
|
|
37
|
-
TimeoutTuple =
|
|
36
|
+
TimeoutTuple = tuple[Timeout, Timeout]
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding=utf-8
|
|
2
1
|
""" A Pythonic implementation of pdsh powered by sshreader
|
|
3
2
|
"""
|
|
4
3
|
# Copyright (C) 2015-2025 Jesse Almanrode
|
|
@@ -16,17 +15,20 @@
|
|
|
16
15
|
# You should have received a copy of the GNU Lesser General Public License
|
|
17
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
17
|
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
19
21
|
from collections import defaultdict
|
|
20
22
|
from hashlib import md5
|
|
21
|
-
|
|
23
|
+
|
|
22
24
|
import click
|
|
23
|
-
import
|
|
24
|
-
|
|
25
|
+
from hostlist import collect_hostlist, expand_hostlist
|
|
26
|
+
|
|
25
27
|
import sshreader
|
|
26
|
-
|
|
28
|
+
|
|
27
29
|
# GLOBALS
|
|
28
30
|
__author__ = 'Jesse Almanrode'
|
|
29
|
-
__version__ = '3.
|
|
31
|
+
__version__ = '3.2.0'
|
|
30
32
|
__examples__ = """\b
|
|
31
33
|
Examples:
|
|
32
34
|
pydsh -w host1,host2,host3 "uname -r"
|
|
@@ -63,10 +65,7 @@ def output(thisjob):
|
|
|
63
65
|
:param thisjob: <ServerJob> object
|
|
64
66
|
:return: None
|
|
65
67
|
"""
|
|
66
|
-
if thisjob.status == 255
|
|
67
|
-
result = thisjob.results[0]
|
|
68
|
-
else:
|
|
69
|
-
result = thisjob.results[0].stdout
|
|
68
|
+
result = thisjob.results[0] if thisjob.status == 255 else thisjob.results[0].stdout
|
|
70
69
|
if len(result) != 0:
|
|
71
70
|
for line in result.split('\n'):
|
|
72
71
|
sshreader.echo(str(thisjob.name) + ': ' + str(line))
|
|
@@ -103,13 +102,10 @@ def coalesce(jobresults):
|
|
|
103
102
|
job_hashes = defaultdict(list)
|
|
104
103
|
output_hashes = dict()
|
|
105
104
|
for job in jobresults:
|
|
106
|
-
if job.status == 255
|
|
107
|
-
result = job.results[0]
|
|
108
|
-
else:
|
|
109
|
-
result = job.results[0].stdout
|
|
105
|
+
result = job.results[0] if job.status == 255 else job.results[0].stdout
|
|
110
106
|
md5sum = md5(result.encode()).hexdigest()
|
|
111
107
|
job_hashes[md5sum].append(job.name)
|
|
112
|
-
if md5sum not in output_hashes
|
|
108
|
+
if md5sum not in output_hashes:
|
|
113
109
|
output_hashes[md5sum] = result
|
|
114
110
|
|
|
115
111
|
for md5sum, stdout in output_hashes.items():
|
|
@@ -130,7 +126,7 @@ def validate_hostlist(ctx, param, value):
|
|
|
130
126
|
try:
|
|
131
127
|
return expand_hostlist(value)
|
|
132
128
|
except Exception:
|
|
133
|
-
raise click.BadOptionUsage(param, 'Invalid hostlist expression')
|
|
129
|
+
raise click.BadOptionUsage(param, 'Invalid hostlist expression') from None
|
|
134
130
|
|
|
135
131
|
|
|
136
132
|
@click.command(epilog=__examples__)
|
|
@@ -255,7 +251,7 @@ def cli(**kwargs):
|
|
|
255
251
|
job.pre_hook = prehook
|
|
256
252
|
jobs.append(job)
|
|
257
253
|
|
|
258
|
-
log.info('Sending
|
|
254
|
+
log.info(f'Sending {len(jobs)} ServerJobs to sshreader module')
|
|
259
255
|
if kwargs['dshbak'] is False and kwargs['coalesce'] is False:
|
|
260
256
|
if kwargs['redline']:
|
|
261
257
|
sshreader.sshread(jobs, pcount=0, tcount=0, print_lock=True)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding=utf-8
|
|
2
1
|
"""A wrapper for Paramiko that attempts to make ssh sessions easier to work with.
|
|
3
2
|
"""
|
|
4
3
|
# Copyright (C) 2015-2025 Jesse Almanrode
|
|
@@ -17,12 +16,14 @@
|
|
|
17
16
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
18
17
|
import logging
|
|
19
18
|
import os
|
|
20
|
-
import paramiko
|
|
21
19
|
import socket
|
|
22
20
|
from getpass import getuser
|
|
23
21
|
from typing import Any, Optional
|
|
24
22
|
|
|
23
|
+
import paramiko
|
|
24
|
+
|
|
25
25
|
from .customtypes import Command, EnvVars, Timeout
|
|
26
|
+
|
|
26
27
|
log = logging.getLogger('sshreader')
|
|
27
28
|
|
|
28
29
|
|
|
@@ -56,7 +57,7 @@ def envvars() -> EnvVars:
|
|
|
56
57
|
return EnvVars(**env)
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
class SSH
|
|
60
|
+
class SSH:
|
|
60
61
|
"""SSH Session class which can be used to send commands to a remote server. It also supports basic SFTP commands
|
|
61
62
|
and can be used to push/pull files from a remote system.
|
|
62
63
|
|
|
@@ -80,16 +81,14 @@ class SSH(object):
|
|
|
80
81
|
"""
|
|
81
82
|
def __init__(self, fqdn: str, username: str, password: Optional[str] = None, keyfile: Optional[str] = None,
|
|
82
83
|
keypass: Optional[str] = None, port: int = 22, connect: bool = True, rsa_sha2: bool = True) -> None:
|
|
83
|
-
if not keyfile:
|
|
84
|
-
|
|
85
|
-
if not all((username, password)):
|
|
86
|
-
paramiko.SSHException('username and password or keyfile not provided')
|
|
84
|
+
if not keyfile and len(paramiko.Agent().get_keys()) == 0 and not all((username, password)):
|
|
85
|
+
paramiko.SSHException('username and password or keyfile not provided')
|
|
87
86
|
self.host = fqdn
|
|
88
87
|
self.username = username
|
|
89
88
|
self.password = password
|
|
90
89
|
if keyfile:
|
|
91
90
|
if not isinstance(keyfile, str):
|
|
92
|
-
raise TypeError('expected
|
|
91
|
+
raise TypeError(f'expected {str(str)} for keyfile, got {str(type(keyfile))}')
|
|
93
92
|
self.keyfile = os.path.abspath(os.path.expanduser(keyfile))
|
|
94
93
|
else:
|
|
95
94
|
self.keyfile = keyfile
|
|
@@ -122,7 +121,7 @@ class SSH(object):
|
|
|
122
121
|
:return: Result of :meth:`paramiko.SFTPClient.put()`
|
|
123
122
|
"""
|
|
124
123
|
if not self.__alive():
|
|
125
|
-
raise paramiko.SSHException("connection to
|
|
124
|
+
raise paramiko.SSHException(f"connection to {self.host} not established")
|
|
126
125
|
sftp = paramiko.SFTPClient.from_transport(self._connection.get_transport())
|
|
127
126
|
try:
|
|
128
127
|
result = sftp.put(os.path.expanduser(srcfile), os.path.expanduser(dstfile))
|
|
@@ -140,7 +139,7 @@ class SSH(object):
|
|
|
140
139
|
:return: None
|
|
141
140
|
"""
|
|
142
141
|
if not self.__alive():
|
|
143
|
-
raise paramiko.SSHException("connection to
|
|
142
|
+
raise paramiko.SSHException(f"connection to {self.host} not established")
|
|
144
143
|
sftp = paramiko.SFTPClient.from_transport(self._connection.get_transport())
|
|
145
144
|
try:
|
|
146
145
|
sftp.get(os.path.expanduser(srcfile), os.path.expanduser(dstfile))
|
|
@@ -164,7 +163,7 @@ class SSH(object):
|
|
|
164
163
|
:raises: :class:`paramiko.SSHException`
|
|
165
164
|
"""
|
|
166
165
|
if self.__alive() is False:
|
|
167
|
-
raise paramiko.SSHException("connection to
|
|
166
|
+
raise paramiko.SSHException(f"connection to {self.host} not established")
|
|
168
167
|
if combine:
|
|
169
168
|
try:
|
|
170
169
|
stdin, stdout, stderr = self._connection.exec_command(command, timeout=timeout, get_pty=True)
|
|
@@ -226,12 +225,10 @@ class SSH(object):
|
|
|
226
225
|
:raises: :class:`paramiko.SSHException`
|
|
227
226
|
"""
|
|
228
227
|
if self.__alive():
|
|
229
|
-
raise paramiko.SSHException("connection to
|
|
228
|
+
raise paramiko.SSHException(f"connection to {self.host} already established")
|
|
230
229
|
paramiko.util.logging.getLogger().setLevel(logging.CRITICAL) # Keeping paramiko from logging errors to stdout
|
|
231
|
-
if not self.keyfile:
|
|
232
|
-
|
|
233
|
-
if not all((self.username, self.password)):
|
|
234
|
-
paramiko.SSHException('username and password or keyfile not provided')
|
|
230
|
+
if not self.keyfile and len(paramiko.Agent().get_keys()) == 0 and not all((self.username, self.password)):
|
|
231
|
+
paramiko.SSHException('username and password or keyfile not provided')
|
|
235
232
|
if self.keyfile:
|
|
236
233
|
if self.rsa_sha2:
|
|
237
234
|
self._connection.connect(self.host, port=self.port, username=self.username, password=self.keypass,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# coding=utf-8
|
|
2
1
|
""" All the classes and functions that make sshreader tick
|
|
3
2
|
"""
|
|
4
3
|
# Copyright (C) 2015-2025 Jesse Almanrode
|
|
@@ -18,17 +17,17 @@
|
|
|
18
17
|
import logging
|
|
19
18
|
import multiprocessing
|
|
20
19
|
import os
|
|
21
|
-
import paramiko
|
|
22
20
|
import subprocess
|
|
23
21
|
import sys
|
|
24
22
|
import threading
|
|
25
23
|
import time
|
|
26
|
-
from progressbar import ProgressBar
|
|
27
24
|
from typing import Any, Callable, Optional, Union
|
|
28
25
|
|
|
26
|
+
import paramiko
|
|
27
|
+
from progressbar import ProgressBar
|
|
29
28
|
|
|
30
|
-
from .ssh import SSH
|
|
31
29
|
from .customtypes import Command, Timeout, TimeoutTuple
|
|
30
|
+
from .ssh import SSH
|
|
32
31
|
|
|
33
32
|
# Globals
|
|
34
33
|
mpctx = multiprocessing.get_context('spawn') # Forcing the forking type to spawn in older versions of Python3
|
|
@@ -68,7 +67,7 @@ def shell_command(command: str, combine: bool = False, decode_bytes: bool = True
|
|
|
68
67
|
return result
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
class Hook
|
|
70
|
+
class Hook:
|
|
72
71
|
""" Custom class for creating "Hooks" that can execute code before of after a ServerJob object executes and
|
|
73
72
|
can evn act on the data of a ServerJob when it is passed as the first argument to the Hook object.
|
|
74
73
|
|
|
@@ -117,7 +116,7 @@ class Hook(object):
|
|
|
117
116
|
return str(self.__dict__)
|
|
118
117
|
|
|
119
118
|
|
|
120
|
-
class ServerJob
|
|
119
|
+
class ServerJob:
|
|
121
120
|
""" Custom class for holding all the info needed to run ssh commands or shell commands in sub-processes or threads
|
|
122
121
|
|
|
123
122
|
:param fqdn: Fully qualified domain name or IP address
|
|
@@ -194,9 +193,8 @@ class ServerJob(object):
|
|
|
194
193
|
self.post_hook = post_hook
|
|
195
194
|
if run_local:
|
|
196
195
|
self._conn = 'localhost'
|
|
197
|
-
elif not keyfile and len(paramiko.Agent().get_keys()) == 0:
|
|
198
|
-
|
|
199
|
-
raise paramiko.SSHException('username and password or ssh key not provided')
|
|
196
|
+
elif not keyfile and len(paramiko.Agent().get_keys()) == 0 and not all([username, password]):
|
|
197
|
+
raise paramiko.SSHException('username and password or ssh key not provided')
|
|
200
198
|
|
|
201
199
|
def run(self) -> int:
|
|
202
200
|
"""Run a ServerJob. SSH to server, run cmds, return result
|
|
@@ -204,54 +202,54 @@ class ServerJob(object):
|
|
|
204
202
|
:return: Sum of return codes for each command executed
|
|
205
203
|
:rtype: int
|
|
206
204
|
"""
|
|
207
|
-
log.info('
|
|
205
|
+
log.info(f'{self.name}: starting ServerJob')
|
|
208
206
|
if self.run_local:
|
|
209
207
|
if self.pre_hook:
|
|
210
|
-
log.debug('
|
|
208
|
+
log.debug(f'{self.name}: running prehook')
|
|
211
209
|
self.pre_hook.run(self)
|
|
212
210
|
for cmd in self.cmds:
|
|
213
211
|
result = shell_command(cmd, combine=self.combine_output)
|
|
214
|
-
log.debug('
|
|
212
|
+
log.debug(f'{self.name}: {str(result)}')
|
|
215
213
|
self.results.append(result)
|
|
216
214
|
self.status += result.return_code
|
|
217
215
|
if self.post_hook:
|
|
218
|
-
log.debug('
|
|
216
|
+
log.debug(f'{self.name}; running posthook')
|
|
219
217
|
self.post_hook.run(self)
|
|
220
218
|
else:
|
|
221
219
|
if self.pre_hook and self.pre_hook.ssh_established is False:
|
|
222
|
-
log.debug('
|
|
220
|
+
log.debug(f'{self.name}: running prehook')
|
|
223
221
|
self.pre_hook.run(self)
|
|
224
222
|
try:
|
|
225
223
|
self._conn = SSH(self.name, username=self.username, password=self.password, keyfile=self.key,
|
|
226
224
|
port=self.ssh_port, connect=False, rsa_sha2=self.rsa_sha2)
|
|
227
225
|
self._conn.connect(timeout=self.ssh_timeout)
|
|
228
|
-
log.debug('
|
|
226
|
+
log.debug(f'{self.name}: ssh connection established')
|
|
229
227
|
except Exception as errorMsg:
|
|
230
228
|
log.debug(str(errorMsg))
|
|
231
229
|
self.status = 255
|
|
232
230
|
self.results.append(str(errorMsg))
|
|
233
231
|
else:
|
|
234
232
|
if self.pre_hook and self.pre_hook.ssh_established:
|
|
235
|
-
log.debug('
|
|
233
|
+
log.debug(f'{self.name}: running prehook')
|
|
236
234
|
self.pre_hook.run(self)
|
|
237
235
|
for cmd in self.cmds:
|
|
238
236
|
try:
|
|
239
237
|
result = self._conn.ssh_command(cmd, timeout=self.cmd_timeout, combine=self.combine_output)
|
|
240
238
|
except Exception as errorMsg:
|
|
241
239
|
result = Command(cmd, '', str(errorMsg), 54)
|
|
242
|
-
log.debug('
|
|
240
|
+
log.debug(f'{self.name}: {str(result)}')
|
|
243
241
|
self.results.append(result)
|
|
244
242
|
self.status += result.return_code
|
|
245
243
|
if self.post_hook and self.post_hook.ssh_established:
|
|
246
|
-
log.debug('
|
|
244
|
+
log.debug(f'{self.name}; running posthook')
|
|
247
245
|
self.post_hook.run(self)
|
|
248
246
|
self._conn.close()
|
|
249
247
|
finally:
|
|
250
248
|
self._conn = None # So the ssh connection can be pickled!
|
|
251
249
|
if self.post_hook and self.post_hook.ssh_established is False:
|
|
252
|
-
log.debug('
|
|
250
|
+
log.debug(f'{self.name}; running posthook')
|
|
253
251
|
self.post_hook.run(self)
|
|
254
|
-
log.info('
|
|
252
|
+
log.info(f'{self.name}: exiting ServerJob')
|
|
255
253
|
return self.status
|
|
256
254
|
|
|
257
255
|
def __str__(self) -> str:
|
|
@@ -361,7 +359,7 @@ def sshread(serverjobs: list, pcount: Optional[int] = None, tcount: Optional[int
|
|
|
361
359
|
else:
|
|
362
360
|
tcount = int(min(tcount, totaljobs))
|
|
363
361
|
|
|
364
|
-
log.info(
|
|
362
|
+
log.info(f'spawning {tcount} threads')
|
|
365
363
|
# Start a thread pool
|
|
366
364
|
for thread in range(tcount):
|
|
367
365
|
thread = threading.Thread(target=_sub_thread_, args=(task_queue, result_queue, item_counter, progress_bar),
|
|
@@ -389,7 +387,7 @@ def sshread(serverjobs: list, pcount: Optional[int] = None, tcount: Optional[int
|
|
|
389
387
|
# If we don't have enough jobs to spawn more than 1 thread per process, then we won't spawn threads
|
|
390
388
|
tcount = 0
|
|
391
389
|
|
|
392
|
-
log.info(
|
|
390
|
+
log.info(f'spawning {pcount} processes')
|
|
393
391
|
for pid in range(pcount):
|
|
394
392
|
pid = mpctx.Process(target=_sub_process_,
|
|
395
393
|
args=(task_queue, result_queue, item_counter, tcount, progress_bar),
|
|
@@ -398,7 +396,7 @@ def sshread(serverjobs: list, pcount: Optional[int] = None, tcount: Optional[int
|
|
|
398
396
|
pids.append(pid)
|
|
399
397
|
|
|
400
398
|
# Non-blocking way to wait for threads/processes
|
|
401
|
-
log.debug('main waiting for
|
|
399
|
+
log.debug(f'main waiting for {totaljobs} ServerJobs to finish')
|
|
402
400
|
while result_queue.full() is False:
|
|
403
401
|
if progress_bar:
|
|
404
402
|
bar.update(item_counter.value)
|
|
@@ -407,12 +405,12 @@ def sshread(serverjobs: list, pcount: Optional[int] = None, tcount: Optional[int
|
|
|
407
405
|
bar.finish()
|
|
408
406
|
|
|
409
407
|
if len(threads) > 0:
|
|
410
|
-
log.info('joining
|
|
408
|
+
log.info(f'joining {len(threads)} threads')
|
|
411
409
|
for t in threads:
|
|
412
410
|
if t.is_alive():
|
|
413
411
|
t.join(timeout=1)
|
|
414
412
|
elif len(pids) > 0:
|
|
415
|
-
log.info('joining
|
|
413
|
+
log.info(f'joining {len(pids)} processes')
|
|
416
414
|
for p in pids:
|
|
417
415
|
if p.is_alive():
|
|
418
416
|
p.join(timeout=1)
|
|
@@ -433,7 +431,7 @@ def _sub_process_(task_queue, result_queue, item_counter, thread_count, progress
|
|
|
433
431
|
DO NOT USE THIS METHOD!
|
|
434
432
|
"""
|
|
435
433
|
pid = os.getpid()
|
|
436
|
-
log.debug(
|
|
434
|
+
log.debug(f'starting process: {pid}')
|
|
437
435
|
if thread_count == 0:
|
|
438
436
|
while task_queue.empty() is False:
|
|
439
437
|
job = task_queue.get()
|
|
@@ -444,16 +442,16 @@ def _sub_process_(task_queue, result_queue, item_counter, thread_count, progress
|
|
|
444
442
|
item_counter.value += 1
|
|
445
443
|
else:
|
|
446
444
|
threads = list()
|
|
447
|
-
log.debug(
|
|
445
|
+
log.debug(f'process: {pid} spawning: {thread_count} threads')
|
|
448
446
|
for thread in range(thread_count):
|
|
449
447
|
thread = threading.Thread(target=_sub_thread_, args=(task_queue, result_queue, item_counter, progress_bar),
|
|
450
448
|
daemon=True)
|
|
451
449
|
thread.start()
|
|
452
450
|
threads.append(thread)
|
|
453
|
-
log.debug(
|
|
451
|
+
log.debug(f'process: {pid} waiting for: {len(threads)} threads')
|
|
454
452
|
for thread in threads:
|
|
455
453
|
thread.join()
|
|
456
|
-
log.debug(
|
|
454
|
+
log.debug(f'exiting process: {pid}')
|
|
457
455
|
return None
|
|
458
456
|
|
|
459
457
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-----BEGIN RSA PRIVATE KEY-----
|
|
2
|
+
MIIEpAIBAAKCAQEAvJzuoZadl3gmPy+4f7mMtS/jpbHH7uxIih6aLRGNugF0Q/Xq
|
|
3
|
+
8bS85mMBlHnz6lu6bDNiLa/Tq4gg8/T9LZ5WTfHWV2Eb4IKiop9k93/GkgSUN7tW
|
|
4
|
+
Ig2GzvSgkqEU5SfR0C4V4N+DhJw5mi40AAEJtc1MN7wHvQ9YctV0L4800OZsn0xl
|
|
5
|
+
LlfIruiP2QhaQ1LY1j0nuevHONxVN65zyOMF9pwp0WdJTiCyMx8NKcWxyi1GNOR+
|
|
6
|
+
1GdANUP7DXRCOUN/JRJIQ2T04xdg+xIJLsEfNRdp8GKR7Gc7b/uwf2iBXgPhQ1pS
|
|
7
|
+
s3yJOV+1WGG6xJ8RCm5Fw58cP/JTolbeRRFvhQIDAQABAoIBAEP+pyBltcZHMDKT
|
|
8
|
+
N4YEFC+1ZfHz10nQsI3qXlZk/5zQrWNaoiokp/y+MHGgTUhtbRVYqMojmsE5v/dy
|
|
9
|
+
mFr7u805o7W4cl5Leen6YGHBFIrLT8uPo4t/KNwu4Dw8CdZato7ALm705OrHjxSA
|
|
10
|
+
F4rw1NlPe2yUQo3rnE05mjsS+01bRWJ8/K+EazDXAiVyOhcWnX/Q7yafhtwqelen
|
|
11
|
+
sQiVChVk419SM/WZcqC9eeYVxM6JZRUrwV1ntKF4/8brwKazwUibsOhlrpqMB7yC
|
|
12
|
+
HGR27XTwCpKHyBfFtc9XZOn15Nig5weHzcu9pNOlpNiG7Pn4jifSdU8q+9hEdZj+
|
|
13
|
+
lRnr7zkCgYEA7U3Wd0i8ta3Z6os9DAMBuVJrPcyr4RQCkt48fH/CVrEh+IhC/gql
|
|
14
|
+
OEMMmaIoarUI6yJikV7hOBJTn33EhyTMjg3I4jL8mxKLO50557uOuiOmfllX0BHD
|
|
15
|
+
91HzBPKPju3nxVGHhPeUIlBpGjdalLY5sRGF3jIdCO2yggyJ2P7XGIcCgYEAy3kM
|
|
16
|
+
tCNbM7a5L6WZ5pWbQNyqoqfO6WtrMCIpeAfEW9rYY2MLE6Lc7bPTUQ5NsSMpNtUK
|
|
17
|
+
FLbku90CrYWi3OG//rfWPnhbchr7CJpwKjh0O3YQrzrfCj17RBAFdcUIHbsjPh17
|
|
18
|
+
SBf489gvUFGYXyZTbg3sDgGLUWq7+To3k3lnVpMCgYEAhdjif9N6BSty/3Tb0Pud
|
|
19
|
+
LE9Nh8T6qXBk5zwMd9iD3+v3EjyhNLMCb9J5t+oby9wilKWHXSFb/QlxO0Nv5x4d
|
|
20
|
+
dn5V8Sf7DOiPuec7svY6Uckr7pYsDoMGumfxn7fEu0R69vO+cYRHfHdIYO4Z3ElQ
|
|
21
|
+
DwyoKiSv9cpEZk/2LHvJDN8CgYB1JlPnH3gyNSE4SqtqdHa4CJUCI4XhJOTJCNJB
|
|
22
|
+
do9ap4zw5CSLgLic721afNbEh08Cb/Rm6sqVU3FoQMn9TMkDOgtTWm6kKt6gHHM6
|
|
23
|
+
C9j4J0kB6u5h8fYXwPWz0eaTX5yx9b12S78dfjn3j5MgLWvZsJBZnA//p2lk3F0F
|
|
24
|
+
Pt/9mwKBgQCCRFX8Cgwkz6S+7kIWExZ+Dt3RtpKD+caZIoJaRZ4yHGAB+dHjk/G+
|
|
25
|
+
PDuGGnHRjFU/R20YLm19b+nlwJiE/LcqA+esOyDaOyLCC1H87b0vJVzrqD2XPogL
|
|
26
|
+
zNRVIA1Uz/d5q4eCLP5jMCIPEDXQq34uXYVtzn7A0RG7EU9KCxVhyA==
|
|
27
|
+
-----END RSA PRIVATE KEY-----
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8nO6hlp2XeCY/L7h/uYy1L+Olscfu7EiKHpotEY26AXRD9erxtLzmYwGUefPqW7psM2Itr9OriCDz9P0tnlZN8dZXYRvggqKin2T3f8aSBJQ3u1YiDYbO9KCSoRTlJ9HQLhXg34OEnDmaLjQAAQm1zUw3vAe9D1hy1XQvjzTQ5myfTGUuV8iu6I/ZCFpDUtjWPSe568c43FU3rnPI4wX2nCnRZ0lOILIzHw0pxbHKLUY05H7UZ0A1Q/sNdEI5Q38lEkhDZPTjF2D7EgkuwR81F2nwYpHsZztv+7B/aIFeA+FDWlKzfIk5X7VYYbrEnxEKbkXDnxw/8lOiVt5FEW+F
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
""" Integration and Unit tests for pydsh script
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
import warnings
|
|
9
|
+
from click.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
__author__ = 'Jesse Almanrode (jesse@almanrode.com)'
|
|
12
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
sys.path.append(project_root)
|
|
14
|
+
import sshreader.scripts.pydsh as pydsh
|
|
15
|
+
|
|
16
|
+
global ssh_data
|
|
17
|
+
# Defaults for testing with Docker!
|
|
18
|
+
ssh_data = {"host_fqdn": "127.0.0.1", "host_port": os.getenv('SSH_PORT', 22),
|
|
19
|
+
"ssh_user": "sshreader", "ssh_password": "sunshine",
|
|
20
|
+
"ssh_key_path": project_root + "/tests/keys/id_rsa"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestPydsh(unittest.TestCase):
|
|
24
|
+
""" Test cases for the pydsh cli
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def setUp(self) -> None:
|
|
28
|
+
self.cli = CliRunner()
|
|
29
|
+
|
|
30
|
+
def test_help(self):
|
|
31
|
+
self.assertEqual(self.cli.invoke(pydsh.cli, ['--help']).exit_code, 0)
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def test_password_auth(self):
|
|
35
|
+
global ssh_data
|
|
36
|
+
cli_result = self.cli.invoke(pydsh.cli, ['-w', ssh_data['host_fqdn'], '--port', ssh_data['host_port'],
|
|
37
|
+
'-u', ssh_data['ssh_user'], '-P', ssh_data['ssh_password'],
|
|
38
|
+
'uname'])
|
|
39
|
+
self.assertIn('Linux', cli_result.stdout)
|
|
40
|
+
self.assertEqual(cli_result.exit_code, 0)
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def test_keyfile_auth(self):
|
|
44
|
+
global ssh_data
|
|
45
|
+
cli_result = self.cli.invoke(pydsh.cli, ['-w', ssh_data['host_fqdn'], '--port', ssh_data['host_port'],
|
|
46
|
+
'-u', ssh_data['ssh_user'], '-k', ssh_data['ssh_key_path'],
|
|
47
|
+
'uname'])
|
|
48
|
+
self.assertIn('Linux', cli_result.stdout)
|
|
49
|
+
self.assertEqual(cli_result.exit_code, 0)
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def test_hostlist_port(self):
|
|
53
|
+
global ssh_data
|
|
54
|
+
host_port = ssh_data['host_fqdn'] + ':' + str(ssh_data['host_port'])
|
|
55
|
+
cli_result = self.cli.invoke(pydsh.cli, ['-w', host_port,
|
|
56
|
+
'-u', ssh_data['ssh_user'], '-k', ssh_data['ssh_key_path'],
|
|
57
|
+
'uname'])
|
|
58
|
+
self.assertIn('Linux', cli_result.stdout)
|
|
59
|
+
self.assertEqual(cli_result.exit_code, 0)
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
def test_dshbak_output(self):
|
|
63
|
+
global ssh_data
|
|
64
|
+
cli_result = self.cli.invoke(pydsh.cli, ['-w', ssh_data['host_fqdn'], '--port', ssh_data['host_port'],
|
|
65
|
+
'-u', ssh_data['ssh_user'], '-k', ssh_data['ssh_key_path'],
|
|
66
|
+
'-D', 'uname'])
|
|
67
|
+
self.assertIn('Linux', cli_result.stdout)
|
|
68
|
+
self.assertEqual(cli_result.exit_code, 0)
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def test_coalesce_output(self):
|
|
72
|
+
global ssh_data
|
|
73
|
+
cli_result = self.cli.invoke(pydsh.cli, ['-w', ssh_data['host_fqdn'], '--port', ssh_data['host_port'],
|
|
74
|
+
'-u', ssh_data['ssh_user'], '-k', ssh_data['ssh_key_path'],
|
|
75
|
+
'-C', 'uname'])
|
|
76
|
+
self.assertIn('Linux', cli_result.stdout)
|
|
77
|
+
self.assertEqual(cli_result.exit_code, 0)
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
if __name__ == '__main__':
|
|
82
|
+
with warnings.catch_warnings(record=True):
|
|
83
|
+
unittest.main()
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# coding=utf-8
|
|
3
|
+
""" Integration and Unit tests for sshreader Python Package
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import unittest
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
__author__ = 'Jesse Almanrode (jesse@almanrode.com)'
|
|
11
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
+
sys.path.append(project_root)
|
|
13
|
+
import sshreader
|
|
14
|
+
|
|
15
|
+
global ssh_data
|
|
16
|
+
# Defaults for testing with Docker!
|
|
17
|
+
ssh_data = {"host_fqdn": "127.0.0.1", "host_port": os.getenv('SSH_PORT', 22),
|
|
18
|
+
"ssh_user": "sshreader", "ssh_password": "sunshine",
|
|
19
|
+
"ssh_key_path": project_root + "/tests/keys/id_rsa"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestMisc(unittest.TestCase):
|
|
23
|
+
""" Test Cases for miscellaneous functions of SSH module
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def test_env_variables(self):
|
|
27
|
+
""" Test envvars method"""
|
|
28
|
+
ssh_env = sshreader.envvars()
|
|
29
|
+
self.assertIsNotNone(ssh_env.username)
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def test_shell_command(self):
|
|
33
|
+
""" Test shell_command method
|
|
34
|
+
"""
|
|
35
|
+
result = sshreader.shell_command('echo "foo"')
|
|
36
|
+
self.assertEqual(result.return_code, 0)
|
|
37
|
+
self.assertIn('foo', result.stdout)
|
|
38
|
+
self.assertEqual(len(result.stderr), 0)
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
def test_shell_command_combined(self):
|
|
42
|
+
""" Test combining stdout and stderr of shell_command method
|
|
43
|
+
"""
|
|
44
|
+
result = sshreader.shell_command('echo "foo"; echo "bar" 1>&2', combine=True)
|
|
45
|
+
self.assertEqual(result.return_code, 0)
|
|
46
|
+
self.assertIn('foo', result.stdout)
|
|
47
|
+
self.assertIn('bar', result.stdout)
|
|
48
|
+
self.assertEqual(result.stderr, None)
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def test_shell_command_stderr(self):
|
|
52
|
+
""" Test stderr of shell_command method
|
|
53
|
+
"""
|
|
54
|
+
result = sshreader.shell_command('echo "bar" 1>&2')
|
|
55
|
+
self.assertEqual(result.return_code, 0)
|
|
56
|
+
self.assertEqual(len(result.stdout), 0)
|
|
57
|
+
self.assertIn('bar', result.stderr)
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def test_decode_bytes(self):
|
|
61
|
+
""" Test to ensure that result is a unicode string type
|
|
62
|
+
"""
|
|
63
|
+
result = sshreader.shell_command('uname -a')
|
|
64
|
+
self.assertEqual(result.return_code, 0)
|
|
65
|
+
self.assertIsInstance(result.stdout, str)
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def test_bytes(self):
|
|
69
|
+
""" Test to ensure that result is a bytes string type
|
|
70
|
+
"""
|
|
71
|
+
result = sshreader.shell_command('uname -a', decode_bytes=False)
|
|
72
|
+
self.assertEqual(result.return_code, 0)
|
|
73
|
+
self.assertIsInstance(result.stdout, bytes)
|
|
74
|
+
result = sshreader.shell_command('uname -a', combine=True, decode_bytes=False)
|
|
75
|
+
self.assertEqual(result.return_code, 0)
|
|
76
|
+
self.assertIsInstance(result.stdout, bytes)
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestSSH(unittest.TestCase):
|
|
81
|
+
""" Test cases for the SSH class
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def test_password(self):
|
|
85
|
+
""" Test an SSH connection using a password
|
|
86
|
+
"""
|
|
87
|
+
global ssh_data
|
|
88
|
+
with sshreader.SSH(ssh_data['host_fqdn'], port=ssh_data['host_port'], username=ssh_data['ssh_user'],
|
|
89
|
+
password=ssh_data['ssh_password']) as conn:
|
|
90
|
+
self.assertTrue(conn.alive())
|
|
91
|
+
self.assertEqual(conn.ssh_command('uname').stdout, 'Linux')
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
def test_keyfile(self):
|
|
95
|
+
""" Test an SSH connection using an ssh key
|
|
96
|
+
"""
|
|
97
|
+
global ssh_data
|
|
98
|
+
with sshreader.SSH(ssh_data['host_fqdn'], port=ssh_data['host_port'], username=ssh_data['ssh_user'],
|
|
99
|
+
keyfile=ssh_data['ssh_key_path']) as conn:
|
|
100
|
+
self.assertTrue(conn.alive())
|
|
101
|
+
self.assertEqual(conn.ssh_command('uname').stdout, 'Linux')
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def test_reconnect(self):
|
|
105
|
+
""" Test re-opening an SSH connection
|
|
106
|
+
"""
|
|
107
|
+
global ssh_data
|
|
108
|
+
conn = sshreader.SSH(ssh_data['host_fqdn'], port=ssh_data['host_port'], username=ssh_data['ssh_user'],
|
|
109
|
+
password=ssh_data['ssh_password'], connect=False)
|
|
110
|
+
self.assertFalse(conn.alive())
|
|
111
|
+
conn.reconnect()
|
|
112
|
+
self.assertTrue(conn.alive())
|
|
113
|
+
conn.close()
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def test_command(self):
|
|
117
|
+
""" Test commands over ssh
|
|
118
|
+
"""
|
|
119
|
+
with sshreader.SSH(ssh_data['host_fqdn'], port=ssh_data['host_port'], username=ssh_data['ssh_user'],
|
|
120
|
+
password=ssh_data['ssh_password']) as conn:
|
|
121
|
+
cmd = conn.ssh_command('uname')
|
|
122
|
+
self.assertEqual(cmd.stdout, 'Linux')
|
|
123
|
+
self.assertEqual(cmd.stderr, '')
|
|
124
|
+
cmd = conn.ssh_command('uname 1>&2')
|
|
125
|
+
self.assertEqual(cmd.stderr, 'Linux')
|
|
126
|
+
self.assertEqual(cmd.stdout, '')
|
|
127
|
+
cmd = conn.ssh_command('echo foo; echo bar 1>&2;', combine=True)
|
|
128
|
+
self.assertIn('foo', cmd.stdout)
|
|
129
|
+
self.assertIn('bar', cmd.stdout)
|
|
130
|
+
self.assertIsNone(cmd.stderr)
|
|
131
|
+
cmd = conn.ssh_command('sleep 5', timeout=2)
|
|
132
|
+
self.assertEqual(cmd.return_code, 124)
|
|
133
|
+
self.assertIn('command timed out', cmd.stderr)
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def my_hook(*args):
|
|
138
|
+
""" Function for testing hook
|
|
139
|
+
:param args: Args should be ('pre|post', sshreader.ServerJob)
|
|
140
|
+
:return:
|
|
141
|
+
"""
|
|
142
|
+
args = list(args)
|
|
143
|
+
if len(args) == 1:
|
|
144
|
+
if args[0] in ('pre', 'post') and isinstance(args.pop(), sshreader.ServerJob):
|
|
145
|
+
return True
|
|
146
|
+
else:
|
|
147
|
+
return False
|
|
148
|
+
else:
|
|
149
|
+
if args[0] in ('pre', 'post'):
|
|
150
|
+
return True
|
|
151
|
+
else:
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TestSshreader(unittest.TestCase):
|
|
156
|
+
""" Test cases for the sshreader module
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def configure_serverjob_list(size):
|
|
161
|
+
""" Configure a list of serverjob objects to sshread (including pre and post hooks) and local commands
|
|
162
|
+
:return: List
|
|
163
|
+
"""
|
|
164
|
+
global ssh_data
|
|
165
|
+
pre = sshreader.Hook(my_hook, args=['pre'])
|
|
166
|
+
post = sshreader.Hook(my_hook, args=['post'])
|
|
167
|
+
jobs = list()
|
|
168
|
+
for x in range(size):
|
|
169
|
+
x = sshreader.ServerJob(ssh_data['host_fqdn'], ['sleep 1', 'echo done'], pre_hook=pre, post_hook=post,
|
|
170
|
+
username=ssh_data['ssh_user'], password=ssh_data['ssh_password'],
|
|
171
|
+
ssh_port=ssh_data['host_port'])
|
|
172
|
+
jobs.append(x)
|
|
173
|
+
for x in range(size):
|
|
174
|
+
jobs.append(sshreader.ServerJob('local-' + str(x), ['sleep 1', 'echo done'], run_local=True))
|
|
175
|
+
return jobs
|
|
176
|
+
|
|
177
|
+
def test_Hook_creation(self):
|
|
178
|
+
""" Test valid hook creation
|
|
179
|
+
"""
|
|
180
|
+
myhook = sshreader.Hook(my_hook, args=['pre'])
|
|
181
|
+
self.assertIsInstance(myhook, sshreader.Hook)
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def test_ServerJob(self):
|
|
185
|
+
""" Test valid ServerJob creation
|
|
186
|
+
"""
|
|
187
|
+
global ssh_data
|
|
188
|
+
job = sshreader.ServerJob(ssh_data['host_fqdn'], 'echo foo', ssh_port=ssh_data['host_port'],
|
|
189
|
+
username=ssh_data['ssh_user'], password=ssh_data['ssh_password'])
|
|
190
|
+
self.assertIsInstance(job, sshreader.ServerJob)
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
def test_ServerJob_with_hooks(self):
|
|
194
|
+
""" Test ServerJob with hooks
|
|
195
|
+
"""
|
|
196
|
+
global ssh_data
|
|
197
|
+
pre = sshreader.Hook(my_hook, args=['pre'])
|
|
198
|
+
post = sshreader.Hook(my_hook, args=['post'])
|
|
199
|
+
job = sshreader.ServerJob(ssh_data['host_fqdn'], 'echo foo', pre_hook=pre, post_hook=post,
|
|
200
|
+
username=ssh_data['ssh_user'], password=ssh_data['ssh_password'],
|
|
201
|
+
ssh_port=ssh_data['host_port'])
|
|
202
|
+
self.assertIsInstance(job, sshreader.ServerJob)
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def test_sshread_threads(self):
|
|
206
|
+
""" Test sshread method using threads
|
|
207
|
+
"""
|
|
208
|
+
jobs = self.configure_serverjob_list(1)
|
|
209
|
+
result = sshreader.sshread(jobs, tcount=0)
|
|
210
|
+
for x in result:
|
|
211
|
+
self.assertEqual(x.status, 0, msg=x.results)
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
def test_sshread_processes(self):
|
|
215
|
+
""" Test sshread method using processes
|
|
216
|
+
"""
|
|
217
|
+
jobs = self.configure_serverjob_list(1)
|
|
218
|
+
result = sshreader.sshread(jobs, pcount=0)
|
|
219
|
+
for x in result:
|
|
220
|
+
self.assertEqual(x.status, 0, msg=x.results)
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
def test_sshread(self):
|
|
224
|
+
""" Test sshread method using threads and processes
|
|
225
|
+
"""
|
|
226
|
+
jobs = self.configure_serverjob_list(1)
|
|
227
|
+
result = sshreader.sshread(jobs, pcount=0, tcount=0)
|
|
228
|
+
for x in result:
|
|
229
|
+
self.assertEqual(x.status, 0, msg=x.results)
|
|
230
|
+
pass
|
|
231
|
+
|
|
232
|
+
def test_cpu_limit(self):
|
|
233
|
+
""" Ensure the cpu_limit methods
|
|
234
|
+
"""
|
|
235
|
+
self.assertIsInstance(sshreader.utils.cpu_limit(), int)
|
|
236
|
+
self.assertIsInstance(sshreader.utils.cpu_limit(2), int)
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
if __name__ == '__main__':
|
|
241
|
+
with warnings.catch_warnings(record=True):
|
|
242
|
+
unittest.main()
|
sshreader-6.1.0/pyproject.toml
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
[tool.poetry]
|
|
2
|
-
name = "sshreader"
|
|
3
|
-
version = "6.1.0"
|
|
4
|
-
description = "Multi-threading/processing wrapper for Paramiko"
|
|
5
|
-
authors = ["Jesse Almanrode <jesse@almanrode.com>"]
|
|
6
|
-
license = "LGPL-3.0-or-later"
|
|
7
|
-
readme = "README.md"
|
|
8
|
-
homepage = "http://sshreader.readthedocs.io/"
|
|
9
|
-
repository = "https://bitbucket.org/isaiah1112/sshreader/"
|
|
10
|
-
classifiers = [
|
|
11
|
-
"Programming Language :: Python",
|
|
12
|
-
"Intended Audience :: Developers",
|
|
13
|
-
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
|
14
|
-
"Development Status :: 5 - Production/Stable",
|
|
15
|
-
"Programming Language :: Python",
|
|
16
|
-
"Programming Language :: Python :: 3.9",
|
|
17
|
-
"Programming Language :: Python :: 3.10",
|
|
18
|
-
"Programming Language :: Python :: 3.11",
|
|
19
|
-
"Programming Language :: Python :: 3.12",
|
|
20
|
-
"Programming Language :: Python :: 3.13",
|
|
21
|
-
"Topic :: Software Development :: Libraries",
|
|
22
|
-
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
23
|
-
]
|
|
24
|
-
packages = [{include = "sshreader"}, {include = "sshreader/scripts"}]
|
|
25
|
-
|
|
26
|
-
[tool.poetry.scripts]
|
|
27
|
-
pydsh = 'sshreader.scripts.pydsh:cli'
|
|
28
|
-
|
|
29
|
-
[tool.poetry.dependencies]
|
|
30
|
-
python = ">3.8,<3.14"
|
|
31
|
-
click = "^8"
|
|
32
|
-
colorama = "^0.4"
|
|
33
|
-
paramiko = "^3"
|
|
34
|
-
progressbar2 = "^4"
|
|
35
|
-
python-hostlist = "^1"
|
|
36
|
-
|
|
37
|
-
[tool.poetry.group.dev]
|
|
38
|
-
optional = true
|
|
39
|
-
|
|
40
|
-
[tool.poetry.group.dev.dependencies]
|
|
41
|
-
coverage = "^7"
|
|
42
|
-
discover = "^0.4"
|
|
43
|
-
flake8 = "^5"
|
|
44
|
-
|
|
45
|
-
[tool.poetry.group.docs]
|
|
46
|
-
optional = true
|
|
47
|
-
|
|
48
|
-
[tool.poetry.group.docs.dependencies]
|
|
49
|
-
sphinx = "^7"
|
|
50
|
-
|
|
51
|
-
[build-system]
|
|
52
|
-
requires = ["poetry-core"]
|
|
53
|
-
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
File without changes
|