reemote 0.0.1__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.
- reemote-0.0.1/LICENSE +21 -0
- reemote-0.0.1/PKG-INFO +37 -0
- reemote-0.0.1/README.md +17 -0
- reemote-0.0.1/reemote/__init__.py +0 -0
- reemote-0.0.1/reemote/operation.py +7 -0
- reemote-0.0.1/reemote/operations/__init__.py +0 -0
- reemote-0.0.1/reemote/operations/apk/__init__.py +0 -0
- reemote-0.0.1/reemote/operations/apk/packages.py +66 -0
- reemote-0.0.1/reemote/operations/apk/update.py +45 -0
- reemote-0.0.1/reemote/operations/apk/upgrade.py +45 -0
- reemote-0.0.1/reemote/operations/filesystem/__init__.py +0 -0
- reemote-0.0.1/reemote/operations/filesystem/chown.py +70 -0
- reemote-0.0.1/reemote/operations/filesystem/directory.py +51 -0
- reemote-0.0.1/reemote/pre_order_generator.py +35 -0
- reemote-0.0.1/reemote/printers.py +232 -0
- reemote-0.0.1/reemote/result.py +9 -0
- reemote-0.0.1/reemote/run.py +40 -0
- reemote-0.0.1/reemote/run_command_on_host.py +46 -0
- reemote-0.0.1/reemote/validate_inventory_file_and_get_inventory.py +31 -0
- reemote-0.0.1/reemote/validate_inventory_structure.py +33 -0
- reemote-0.0.1/reemote/validate_root_class_name_and_get_root_class.py +25 -0
- reemote-0.0.1/reemote/verify_inventory_connect.py +15 -0
- reemote-0.0.1/reemote/verify_python_file.py +13 -0
- reemote-0.0.1/reemote/verify_source_file_contains_valid_class.py +39 -0
- reemote-0.0.1/reemote.egg-info/PKG-INFO +37 -0
- reemote-0.0.1/reemote.egg-info/SOURCES.txt +36 -0
- reemote-0.0.1/reemote.egg-info/dependency_links.txt +1 -0
- reemote-0.0.1/reemote.egg-info/entry_points.txt +2 -0
- reemote-0.0.1/reemote.egg-info/requires.txt +14 -0
- reemote-0.0.1/reemote.egg-info/top_level.txt +3 -0
- reemote-0.0.1/reemote_cli/__init__.py +0 -0
- reemote-0.0.1/reemote_cli/reemote.py +77 -0
- reemote-0.0.1/setup.cfg +4 -0
- reemote-0.0.1/setup.py +40 -0
- reemote-0.0.1/tests/__init__.py +0 -0
- reemote-0.0.1/tests/filesystem/__init__.py +0 -0
- reemote-0.0.1/tests/filesystem/test_chown.py +144 -0
- reemote-0.0.1/tests/filesystem/test_directory.py +67 -0
reemote-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kim Jarvis
|
|
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.
|
reemote-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reemote
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Python package for reemote functionality
|
|
5
|
+
Home-page: https://github.com/kimjarvis/reemote
|
|
6
|
+
Author: Kim Jarvis
|
|
7
|
+
Author-email: kim.jarvis@tpfsystems.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.6
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: cryptography
|
|
13
|
+
Requires-Dist: bcrypt
|
|
14
|
+
Requires-Dist: asyncssh
|
|
15
|
+
Requires-Dist: tabulate
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
18
|
+
Requires-Dist: asyncssh; extra == "dev"
|
|
19
|
+
Provides-Extra: doc
|
|
20
|
+
Requires-Dist: sphinx; extra == "doc"
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: author
|
|
24
|
+
Dynamic: author-email
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: home-page
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
Dynamic: provides-extra
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Reemote is a Python API for task automation, configuration management and application deployment.
|
|
37
|
+
|
reemote-0.0.1/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Reemote
|
|
2
|
+
|
|
3
|
+
Reemote is a Python API for task automation, configuration management and application deployment.
|
|
4
|
+
|
|
5
|
+
# Example
|
|
6
|
+
|
|
7
|
+
The [GUI example](examples/gui) demonstrates a switch that creates or removes the directory `/tmp/mydir` on all the servers in an inventory.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
# Installation
|
|
12
|
+
|
|
13
|
+
Install from PyPI
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install reemote
|
|
17
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from reemote.printers import print_ssh_completed_process
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
class Packages:
|
|
5
|
+
"""
|
|
6
|
+
A class to manage package operations on a remote system using `apk` (Alpine Linux package manager).
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
packages (List[str]): A list of package names to be added or removed.
|
|
10
|
+
present (bool): Indicates whether the packages should be present (`True`) or absent (`False`) on the system.
|
|
11
|
+
sudo (bool): If `True`, the commands will be executed with `sudo` privileges.
|
|
12
|
+
su (bool): If `True`, the commands will be executed with `su` privileges.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
This class is designed to be used in a generator-based workflow where commands are yielded for execution.
|
|
16
|
+
It supports adding or removing packages based on the `present` flag and allows privilege escalation via `sudo` or `su`.
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
- Commands are constructed based on the `present`, `sudo`, and `su` flags.
|
|
20
|
+
- The `changed` flag is set if the package state changes after execution.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self,
|
|
24
|
+
packages: List[str],
|
|
25
|
+
present: bool,
|
|
26
|
+
repository: str = None ,
|
|
27
|
+
sudo: bool = False,
|
|
28
|
+
su: bool = False):
|
|
29
|
+
self.packages: List[str] = packages
|
|
30
|
+
self.present: bool = present
|
|
31
|
+
self.repository: str = repository
|
|
32
|
+
self.sudo: bool = sudo
|
|
33
|
+
self.su: bool = su
|
|
34
|
+
|
|
35
|
+
# Construct the operation string from the list of packages
|
|
36
|
+
op: List[str] = []
|
|
37
|
+
op.extend(self.packages)
|
|
38
|
+
if repository:
|
|
39
|
+
op.append(f"--repository {repository}")
|
|
40
|
+
self.op: str = " ".join(op)
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
return (f"Packages(packages={self.packages!r}, present={self.present!r},"
|
|
44
|
+
f"repository={self.repository!r},"
|
|
45
|
+
f"sudo={self.sudo!r}, su={self.su!r})")
|
|
46
|
+
|
|
47
|
+
def execute(self):
|
|
48
|
+
r0 = yield f"echo {self}"
|
|
49
|
+
_sudo: str = "sudo -S " if self.sudo else ""
|
|
50
|
+
_su: str = "su -c " if self.su else ""
|
|
51
|
+
|
|
52
|
+
# Retrieve the current list of installed packages
|
|
53
|
+
r1 = yield f"{_sudo}apk info -v"
|
|
54
|
+
|
|
55
|
+
# Add or remove packages based on the `present` flag
|
|
56
|
+
if self.present:
|
|
57
|
+
r2 = yield f"{_sudo}{_su}'apk add {self.op}'"
|
|
58
|
+
else:
|
|
59
|
+
r2 = yield f"{_sudo}{_su}'apk del {self.op}'"
|
|
60
|
+
|
|
61
|
+
# Retrieve the updated list of installed packages
|
|
62
|
+
r3 = yield f"{_sudo}apk info -v"
|
|
63
|
+
|
|
64
|
+
# Set the `changed` flag if the package state has changed
|
|
65
|
+
if r1.cp.stdout != r3.cp.stdout:
|
|
66
|
+
r2.changed = True
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from reemote.printers import print_ssh_completed_process
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
class Update:
|
|
5
|
+
"""
|
|
6
|
+
A class to manage package operations on a remote system using `apk` (Alpine Linux package manager).
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
sudo (bool): If `True`, the commands will be executed with `sudo` privileges.
|
|
10
|
+
su (bool): If `True`, the commands will be executed with `su` privileges.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
Update installed packages.
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
- Commands are constructed based on the `present`, `sudo`, and `su` flags.
|
|
17
|
+
- The `changed` flag is set if the package state changes after execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
sudo: bool = False,
|
|
22
|
+
su: bool = False):
|
|
23
|
+
self.sudo: bool = sudo
|
|
24
|
+
self.su: bool = su
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return (f"Update("
|
|
28
|
+
f"sudo={self.sudo!r}, su={self.su!r})")
|
|
29
|
+
|
|
30
|
+
def execute(self):
|
|
31
|
+
r0 = yield f"echo {self}"
|
|
32
|
+
_sudo: str = "sudo -S " if self.sudo else ""
|
|
33
|
+
_su: str = "su -c " if self.su else ""
|
|
34
|
+
|
|
35
|
+
# Retrieve the current list of installed packages
|
|
36
|
+
r1 = yield f"{_sudo}apk info -v"
|
|
37
|
+
|
|
38
|
+
r2 = yield f"{_sudo}{_su}'apk update'"
|
|
39
|
+
|
|
40
|
+
# Retrieve the updated list of installed packages
|
|
41
|
+
r3 = yield f"{_sudo}apk info -v"
|
|
42
|
+
|
|
43
|
+
# Set the `changed` flag if the package state has changed
|
|
44
|
+
if r1.cp.stdout != r3.cp.stdout:
|
|
45
|
+
r2.changed = True
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from reemote.printers import print_ssh_completed_process
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
class Upgrade:
|
|
5
|
+
"""
|
|
6
|
+
A class to manage package operations on a remote system using `apk` (Alpine Linux package manager).
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
sudo (bool): If `True`, the commands will be executed with `sudo` privileges.
|
|
10
|
+
su (bool): If `True`, the commands will be executed with `su` privileges.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
Upgrade installed packages.
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
- Commands are constructed based on the `present`, `sudo`, and `su` flags.
|
|
17
|
+
- The `changed` flag is set if the package state changes after execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
sudo: bool = False,
|
|
22
|
+
su: bool = False):
|
|
23
|
+
self.sudo: bool = sudo
|
|
24
|
+
self.su: bool = su
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return (f"Upgrade("
|
|
28
|
+
f"sudo={self.sudo!r}, su={self.su!r})")
|
|
29
|
+
|
|
30
|
+
def execute(self):
|
|
31
|
+
r0 = yield f"echo {self}"
|
|
32
|
+
_sudo: str = "sudo -S " if self.sudo else ""
|
|
33
|
+
_su: str = "su -c " if self.su else ""
|
|
34
|
+
|
|
35
|
+
# Retrieve the current list of installed packages
|
|
36
|
+
r1 = yield f"{_sudo}apk info -v"
|
|
37
|
+
|
|
38
|
+
r2 = yield f"{_sudo}{_su}'apk upgrade'"
|
|
39
|
+
|
|
40
|
+
# Retrieve the upgraded list of installed packages
|
|
41
|
+
r3 = yield f"{_sudo}apk info -v"
|
|
42
|
+
|
|
43
|
+
# Set the `changed` flag if the package state has changed
|
|
44
|
+
if r1.cp.stdout != r3.cp.stdout:
|
|
45
|
+
r2.changed = True
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
class Chown:
|
|
2
|
+
"""
|
|
3
|
+
A class to encapsulate the functionality of the `chown` or `chgrp` command in Unix-like operating systems.
|
|
4
|
+
It allows users to specify a target file or directory, along with optional user and group ownership changes,
|
|
5
|
+
additional command-line options, and the ability to execute the command with elevated privileges (`sudo`).
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
target (str): The file or directory whose ownership is to be changed.
|
|
9
|
+
user (Optional[str]): The new user owner. Defaults to `None`.
|
|
10
|
+
group (Optional[str]): The new group owner. Defaults to `None`.
|
|
11
|
+
options (List[str]): Additional command-line options for the `chown` or `chgrp` command.
|
|
12
|
+
sudo (bool): If `True`, the commands will be executed with `sudo` privileges.
|
|
13
|
+
su (bool): If `True`, the commands will be executed with `su` privileges.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, target: str,
|
|
16
|
+
user: str | None = None,
|
|
17
|
+
group: str | None = None,
|
|
18
|
+
options=None,
|
|
19
|
+
sudo: bool = False):
|
|
20
|
+
|
|
21
|
+
self.target = target
|
|
22
|
+
self.user = user
|
|
23
|
+
self.group = group
|
|
24
|
+
self.options = options
|
|
25
|
+
self.sudo = sudo
|
|
26
|
+
|
|
27
|
+
if options is None:
|
|
28
|
+
options = []
|
|
29
|
+
|
|
30
|
+
command = "chown"
|
|
31
|
+
user_group = None
|
|
32
|
+
|
|
33
|
+
if user and group:
|
|
34
|
+
user_group = f"{user}:{group}"
|
|
35
|
+
elif user:
|
|
36
|
+
user_group = user
|
|
37
|
+
elif group:
|
|
38
|
+
command = "chgrp"
|
|
39
|
+
user_group = group
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError("Either user or group must be specified")
|
|
42
|
+
|
|
43
|
+
op = []
|
|
44
|
+
op.append(command)
|
|
45
|
+
op.extend(options)
|
|
46
|
+
op.append(user_group)
|
|
47
|
+
op.append(target)
|
|
48
|
+
|
|
49
|
+
self.chown = " ".join(op)
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return (f"Chown(target={self.target!r}, user={self.user!r}, "
|
|
53
|
+
f"group={self.group!r}, options={self.options!r}, sudo={self.sudo!r})")
|
|
54
|
+
|
|
55
|
+
def execute(self):
|
|
56
|
+
yield f"echo {self}"
|
|
57
|
+
_sudo = "sudo -S " if self.sudo else ""
|
|
58
|
+
|
|
59
|
+
# Get initial file info
|
|
60
|
+
r0 = yield f"{_sudo}ls -ld {self.target}"
|
|
61
|
+
|
|
62
|
+
# Execute chown command
|
|
63
|
+
r1 = yield f"{_sudo}{self.chown}"
|
|
64
|
+
|
|
65
|
+
# Get final file info to check if changed
|
|
66
|
+
r2 = yield f"{_sudo}ls -ld {self.target}"
|
|
67
|
+
|
|
68
|
+
# Set changed flag if the output differs
|
|
69
|
+
if r0.cp.stdout != r2.cp.stdout:
|
|
70
|
+
r1.changed = True
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from reemote.result import Result
|
|
2
|
+
|
|
3
|
+
class Directory:
|
|
4
|
+
class Directory:
|
|
5
|
+
"""
|
|
6
|
+
A class to manage directory states on a filesystem.
|
|
7
|
+
|
|
8
|
+
Attributes:
|
|
9
|
+
path (str): The absolute or relative path of the directory to manage. This is the target directory whose state will be checked or modified.
|
|
10
|
+
present (bool): Indicates whether the directory should exist (`True`) or not (`False`) on the system. If `True`, the directory will be created if it does not exist. If `False`, the directory will be removed if it exists.
|
|
11
|
+
sudo (bool): If `True`, the commands will be executed with `sudo` privileges. Defaults to `False`.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
This class is designed to be used in a generator-based workflow where commands are yielded for execution.
|
|
15
|
+
It supports creating or removing directories based on the `present` flag and allows privilege escalation via `sudo`.
|
|
16
|
+
|
|
17
|
+
Notes:
|
|
18
|
+
- Commands are constructed based on the `present` and `sudo` flags.
|
|
19
|
+
- The `changed` flag is set if the directory state changes after execution.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, path: str, present: bool, sudo: bool = False):
|
|
22
|
+
self.path = path
|
|
23
|
+
self.present = present
|
|
24
|
+
self.sudo = sudo
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return f"Directory(path={self.path!r}, present={self.present!r}, sudo={self.sudo!r})"
|
|
28
|
+
|
|
29
|
+
def execute(self):
|
|
30
|
+
yield f"echo {self}"
|
|
31
|
+
"""
|
|
32
|
+
Execute the directory management logic as a generator.
|
|
33
|
+
|
|
34
|
+
This method checks if the directory exists and performs actions based on the `present` flag.
|
|
35
|
+
"""
|
|
36
|
+
_sudo = "sudo -S " if self.sudo else ""
|
|
37
|
+
# Check whether the directory exists
|
|
38
|
+
r0: Result = yield f"{_sudo}[ -d {self.path} ]"
|
|
39
|
+
# print(f">>>>> Received in Directory: {r}")
|
|
40
|
+
|
|
41
|
+
if self.present and r0.cp.returncode != 0:
|
|
42
|
+
# Present directory does not exist, so create it
|
|
43
|
+
r1 =yield f"{_sudo}mkdir -p {self.path}"
|
|
44
|
+
# print(f">>>>> Received in Directory: {r1}")
|
|
45
|
+
r1.changed=True
|
|
46
|
+
|
|
47
|
+
if not self.present and r0.cp.returncode == 0:
|
|
48
|
+
# Not Present directory exists, so remove it
|
|
49
|
+
r2 = yield f"{_sudo}rmdir -p {self.path}"
|
|
50
|
+
# print(f">>>>> Received in Directory: {r2}")
|
|
51
|
+
r2.changed=True
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from reemote.operation import Operation
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def pre_order_generator(node):
|
|
5
|
+
"""
|
|
6
|
+
Enhanced generator function with better error handling and string wrapping.
|
|
7
|
+
"""
|
|
8
|
+
stack = [(node, iter(node.execute()))]
|
|
9
|
+
result = None
|
|
10
|
+
|
|
11
|
+
while stack:
|
|
12
|
+
current_node, iterator = stack[-1]
|
|
13
|
+
try:
|
|
14
|
+
value = iterator.send(result) if result is not None else next(iterator)
|
|
15
|
+
result = None
|
|
16
|
+
|
|
17
|
+
# Handle different types of yielded values
|
|
18
|
+
if isinstance(value, Operation):
|
|
19
|
+
result = yield value
|
|
20
|
+
elif isinstance(value, str):
|
|
21
|
+
# Auto-wrap strings in Operation objects
|
|
22
|
+
operation = Operation(value)
|
|
23
|
+
result = yield operation
|
|
24
|
+
elif hasattr(value, 'execute') and callable(value.execute):
|
|
25
|
+
# If it's a node with execute method, push to stack
|
|
26
|
+
stack.append((value, iter(value.execute())))
|
|
27
|
+
else:
|
|
28
|
+
raise TypeError(f"Unsupported yield type: {type(value)}")
|
|
29
|
+
|
|
30
|
+
except StopIteration:
|
|
31
|
+
stack.pop()
|
|
32
|
+
except Exception as e:
|
|
33
|
+
# Handle errors in node execution
|
|
34
|
+
print(f"Error in node execution: {e}")
|
|
35
|
+
stack.pop()
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from asyncssh import SSHCompletedProcess
|
|
2
|
+
from asyncssh import ProcessError
|
|
3
|
+
from typing import Optional, Mapping, Tuple, Union
|
|
4
|
+
import json
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from tabulate import tabulate
|
|
7
|
+
|
|
8
|
+
def print_ssh_completed_process(process_result):
|
|
9
|
+
"""
|
|
10
|
+
Prints the fields of an SSHCompletedProcess object.
|
|
11
|
+
|
|
12
|
+
:param process_result: The completed process result to inspect.
|
|
13
|
+
:type process_result: SSHCompletedProcess
|
|
14
|
+
"""
|
|
15
|
+
# Ensure the input is of the expected type
|
|
16
|
+
if not isinstance(process_result, SSHCompletedProcess):
|
|
17
|
+
raise ValueError("The provided argument is not an instance of SSHCompletedProcess.")
|
|
18
|
+
|
|
19
|
+
# Define the fields to extract and their descriptions
|
|
20
|
+
fields = [
|
|
21
|
+
("env", "The environment the client requested to be set for the process", "dict or None"),
|
|
22
|
+
("command", "The command the client requested the process to execute (if any)", "str or None"),
|
|
23
|
+
("subsystem", "The subsystem the client requested the process to open (if any)", "str or None"),
|
|
24
|
+
("exit_status", "The exit status returned, or -1 if an exit signal is sent", "int"),
|
|
25
|
+
("exit_signal", "The exit signal sent (if any) in the form of a tuple containing the signal name, "
|
|
26
|
+
"a bool for whether a core dump occurred, a message associated with the signal, "
|
|
27
|
+
"and the language the message was in", "tuple or None"),
|
|
28
|
+
("returncode", "The exit status returned, or negative of the signal number when an exit signal is sent", "int"),
|
|
29
|
+
("stdout", "The output sent by the process to stdout (if not redirected)", "str or bytes"),
|
|
30
|
+
("stderr", "The output sent by the process to stderr (if not redirected)", "str or bytes"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Print each field and its value
|
|
34
|
+
print("Fields of SSHCompletedProcess:")
|
|
35
|
+
print("--------------------------------")
|
|
36
|
+
for field_name, description, field_type in fields:
|
|
37
|
+
value = getattr(process_result, field_name, None)
|
|
38
|
+
print(f"{field_name}: {value}")
|
|
39
|
+
print(f" Description: {description}")
|
|
40
|
+
print(f" Type: {field_type}")
|
|
41
|
+
print()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_process_error_fields(process_error):
|
|
45
|
+
"""
|
|
46
|
+
Prints the fields of an asyncssh.ProcessError exception.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
process_error (asyncssh.ProcessError): The exception instance to inspect.
|
|
50
|
+
"""
|
|
51
|
+
if not isinstance(process_error, ProcessError):
|
|
52
|
+
raise ValueError("The provided argument is not an instance of asyncssh.ProcessError.")
|
|
53
|
+
|
|
54
|
+
# Define the fields to extract and their descriptions
|
|
55
|
+
fields = [
|
|
56
|
+
("env", "The environment the client requested to be set for the process"),
|
|
57
|
+
("command", "The command the client requested the process to execute (if any)"),
|
|
58
|
+
("subsystem", "The subsystem the client requested the process to open (if any)"),
|
|
59
|
+
("exit_status", "The exit status returned, or -1 if an exit signal is sent"),
|
|
60
|
+
("exit_signal", "The exit signal sent (if any) in the form of a tuple containing the signal name, "
|
|
61
|
+
"a bool for whether a core dump occurred, a message associated with the signal, "
|
|
62
|
+
"and the language the message was in"),
|
|
63
|
+
("returncode", "The exit status returned, or negative of the signal number when an exit signal is sent"),
|
|
64
|
+
("stdout", "The output sent by the process to stdout (if not redirected)"),
|
|
65
|
+
("stderr", "The output sent by the process to stderr (if not redirected)"),
|
|
66
|
+
("reason", "The reason for the error"),
|
|
67
|
+
("lang", "The language of the error message")
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# Print each field and its value
|
|
71
|
+
print("Fields of asyncssh.ProcessError:")
|
|
72
|
+
print("--------------------------------")
|
|
73
|
+
for field_name, description in fields:
|
|
74
|
+
value = getattr(process_error, field_name, None)
|
|
75
|
+
print(f"{field_name}: {value}")
|
|
76
|
+
print(f" Description: {description}")
|
|
77
|
+
print()
|
|
78
|
+
|
|
79
|
+
def construct_host_ops(operations, results):
|
|
80
|
+
"""
|
|
81
|
+
Construct a list of dictionaries grouping operations and results by host.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
operations (list): List of Operation objects.
|
|
85
|
+
results (list): List of Result objects.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
list: A list of dictionaries in the desired format.
|
|
89
|
+
"""
|
|
90
|
+
# Dictionary to group operations and results by host
|
|
91
|
+
host_dict = {}
|
|
92
|
+
|
|
93
|
+
# Iterate over operations and results simultaneously
|
|
94
|
+
for operation, result in zip(operations, results):
|
|
95
|
+
host = operation.host_info['host'] # Extract host from operation
|
|
96
|
+
stdout = result.cp.stdout.strip() if result.cp.stdout else "<no stdout>" # Extract stdout from result
|
|
97
|
+
|
|
98
|
+
# Group by host
|
|
99
|
+
if host not in host_dict:
|
|
100
|
+
host_dict[host] = []
|
|
101
|
+
host_dict[host].append(
|
|
102
|
+
({"command": operation.command},
|
|
103
|
+
{"stdout": stdout, "returncode": result.cp.returncode,
|
|
104
|
+
"changed": result.changed}))
|
|
105
|
+
|
|
106
|
+
# Convert the dictionary into the desired list of dictionaries format
|
|
107
|
+
host_ops_list = [{"host": host, "ops": ops} for host, ops in host_dict.items()]
|
|
108
|
+
|
|
109
|
+
return host_ops_list
|
|
110
|
+
|
|
111
|
+
def print_json_ops(host_ops):
|
|
112
|
+
pretty_output = json.dumps(host_ops, indent=4)
|
|
113
|
+
# Print the formatted output
|
|
114
|
+
print(pretty_output)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def generate_change_df(host_ops):
|
|
118
|
+
# Step 1: Extract commands, changed, and executed values for each host
|
|
119
|
+
data = {}
|
|
120
|
+
for host_data in host_ops:
|
|
121
|
+
host = host_data["host"]
|
|
122
|
+
data[host] = []
|
|
123
|
+
for op_pair in host_data["ops"]:
|
|
124
|
+
# Extract the command from the first dictionary
|
|
125
|
+
command = op_pair[0]["command"]
|
|
126
|
+
# Merge the changed values from both dictionaries
|
|
127
|
+
changed = any(d.get("changed", False) for d in op_pair)
|
|
128
|
+
# Get the executed value from the second dictionary
|
|
129
|
+
executed = op_pair[1].get("executed", False)
|
|
130
|
+
data[host].append((command, changed, executed))
|
|
131
|
+
# Step 2: Align commands across hosts
|
|
132
|
+
commands = [op[0] for op in data[next(iter(data))]] # Use the first host's commands as reference
|
|
133
|
+
df_data = {}
|
|
134
|
+
# Add columns for each host: <host>_changed and <host>_executed
|
|
135
|
+
for host, ops in data.items():
|
|
136
|
+
df_data[(host, "changed")] = [changed for _, changed, _ in ops]
|
|
137
|
+
df_data[(host, "executed")] = [executed for _, _, executed in ops]
|
|
138
|
+
# Step 3: Create the DataFrame with MultiIndex columns
|
|
139
|
+
df = pd.DataFrame(df_data, index=commands)
|
|
140
|
+
# Rename the index to "Command"
|
|
141
|
+
df.index.name = "Command"
|
|
142
|
+
# Display the DataFrame
|
|
143
|
+
return df
|
|
144
|
+
|
|
145
|
+
def convert_df_to_ag_grid(df):
|
|
146
|
+
# Reset index to make the index a column
|
|
147
|
+
df_reset = df.reset_index()
|
|
148
|
+
|
|
149
|
+
# Convert all column names to strings
|
|
150
|
+
df_reset.columns = [str(col) if not isinstance(col, tuple) else ' '.join(map(str, col)).strip()
|
|
151
|
+
for col in df_reset.columns]
|
|
152
|
+
|
|
153
|
+
# Create column definitions
|
|
154
|
+
column_defs = []
|
|
155
|
+
for col in df_reset.columns:
|
|
156
|
+
field_name = col.lower().replace(' ', '_')
|
|
157
|
+
column_defs.append({'headerName': col, 'field': field_name})
|
|
158
|
+
|
|
159
|
+
# Create row data
|
|
160
|
+
row_data = []
|
|
161
|
+
for index, row in df_reset.iterrows():
|
|
162
|
+
row_dict = {}
|
|
163
|
+
for col in df_reset.columns:
|
|
164
|
+
field_name = col.lower().replace(' ', '_')
|
|
165
|
+
row_dict[field_name] = row[col]
|
|
166
|
+
row_data.append(row_dict)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
'columnDefs': column_defs,
|
|
170
|
+
'rowData': row_data
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def summarize_data_for_aggrid(data):
|
|
175
|
+
# Initialize the result dictionary
|
|
176
|
+
result = {"columnDefs": [], "rowData": []}
|
|
177
|
+
|
|
178
|
+
# Extract the list of hosts from the input data
|
|
179
|
+
hosts = [host_data["host"] for host_data in data]
|
|
180
|
+
|
|
181
|
+
# Add the 'Command' column to columnDefs
|
|
182
|
+
result["columnDefs"].append({"headerName": "Command", "field": "command"})
|
|
183
|
+
|
|
184
|
+
# Add a column for each host
|
|
185
|
+
for i, host in enumerate(hosts):
|
|
186
|
+
result["columnDefs"].append({"headerName": host, "field": f"host{i}"})
|
|
187
|
+
|
|
188
|
+
# Populate rowData
|
|
189
|
+
for ops_index, ops in enumerate(data[0]["ops"]): # Assuming all hosts have the same commands
|
|
190
|
+
row = {}
|
|
191
|
+
# Add the command to the row
|
|
192
|
+
if ops[0]["command"].startswith("echo"):
|
|
193
|
+
row["command"] = ops[0]["command"].replace("echo", ">>>>")
|
|
194
|
+
else:
|
|
195
|
+
row["command"] = ops[0]["command"]
|
|
196
|
+
|
|
197
|
+
# Add boolean values for each host
|
|
198
|
+
for host_index, host_data in enumerate(data):
|
|
199
|
+
command_dict, result_dict = host_data["ops"][ops_index]
|
|
200
|
+
changed = result_dict.get("changed", False)
|
|
201
|
+
row[f"host{host_index}"] = changed
|
|
202
|
+
|
|
203
|
+
# Append the row to rowData
|
|
204
|
+
result["rowData"].append(row)
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_printable_aggrid(data):
|
|
210
|
+
"""
|
|
211
|
+
Formats and prints the given AGGrid data as a formatted grid.
|
|
212
|
+
|
|
213
|
+
Parameters:
|
|
214
|
+
data (dict): A dictionary containing 'columnDefs' and 'rowData'.
|
|
215
|
+
- 'columnDefs': List of dictionaries with 'headerName' and 'field'.
|
|
216
|
+
- 'rowData': List of dictionaries representing rows of data.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
None: Prints the formatted grid.
|
|
220
|
+
"""
|
|
221
|
+
# Step 1: Extract headers and field mappings
|
|
222
|
+
headers = [col['headerName'] for col in data['columnDefs']]
|
|
223
|
+
field_mapping = {col['field']: col['headerName'] for col in data['columnDefs']}
|
|
224
|
+
|
|
225
|
+
# Step 2: Prepare the table data
|
|
226
|
+
table_data = []
|
|
227
|
+
for row in data['rowData']:
|
|
228
|
+
formatted_row = [row[field] for field in field_mapping.keys()]
|
|
229
|
+
table_data.append(formatted_row)
|
|
230
|
+
|
|
231
|
+
# Step 3: Print the formatted grid using tabulate
|
|
232
|
+
print(tabulate(table_data, headers=headers, tablefmt="grid"))
|