monitorify 1.0.2__tar.gz → 1.1.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.
- monitorify-1.1.0/PKG-INFO +145 -0
- monitorify-1.1.0/README.md +131 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/pyproject.toml +4 -1
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/__init__.py +1 -1
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config.py +1 -1
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/main.py +28 -34
- monitorify-1.1.0/src/monitorify/remote/__init__.py +4 -0
- monitorify-1.1.0/src/monitorify/remote/addRemote.py +157 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/remote/connector.py +82 -8
- monitorify-1.1.0/src/monitorify/remote/install.sh +81 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/storage/db.py +148 -2
- monitorify-1.1.0/src/monitorify/storage/keyring_helper.py +48 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/components/menu.py +95 -54
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/css/tui.css +82 -6
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/tui.py +94 -1
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/programmListWidget.py +145 -14
- monitorify-1.1.0/src/monitorify.egg-info/PKG-INFO +145 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/SOURCES.txt +7 -1
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/requires.txt +2 -0
- monitorify-1.1.0/tests/test_menu.py +75 -0
- monitorify-1.1.0/tests/test_programm_list_search.py +138 -0
- monitorify-1.1.0/tests/test_remote_db.py +207 -0
- monitorify-1.0.2/PKG-INFO +0 -62
- monitorify-1.0.2/README.md +0 -50
- monitorify-1.0.2/src/monitorify/remote/__init__.py +0 -3
- monitorify-1.0.2/src/monitorify.egg-info/PKG-INFO +0 -62
- {monitorify-1.0.2 → monitorify-1.1.0}/setup.cfg +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/__main__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/manager.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/schema.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/collector/worker.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config/configManager.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/config_manager.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/daemon.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/DiskStats.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/cpuStats.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/networkStats.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/programmList.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/stats/ramStats.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/storage/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/components/widgetManager.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/__init__.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/brailleGraph.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/cpuWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/diskWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/networkWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/procInfoWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/ramWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify/ui/tui/widgets/statusWidget.py +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/dependency_links.txt +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/entry_points.txt +0 -0
- {monitorify-1.0.2 → monitorify-1.1.0}/src/monitorify.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monitorify
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A lightweight Linux terminal system monitor built with Textual
|
|
5
|
+
Author: Leon
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: textual>=0.80.0
|
|
10
|
+
Requires-Dist: platformdirs>=4.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Requires-Dist: paramiko>=3.0.0
|
|
13
|
+
Requires-Dist: keyring>=24.0.0
|
|
14
|
+
|
|
15
|
+
# Monitorify
|
|
16
|
+
### A terminal montioring tool for linux
|
|
17
|
+
|
|
18
|
+
- Features
|
|
19
|
+
- CPU monitoring
|
|
20
|
+
- RAM monitoring
|
|
21
|
+
- Network monitoring
|
|
22
|
+
- Disk monitoring (I/O and storage capacity)
|
|
23
|
+
- Process monitoring
|
|
24
|
+
- Background daemon
|
|
25
|
+
- Customization menu (toggle widgets, set intervals, and change history window)
|
|
26
|
+
- Historical graphs
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
* **Python:** 3.10+
|
|
31
|
+
* **OS:** Linux (relies on `/proc` and `/sys` filesystems)
|
|
32
|
+
* **Terminal:** Minimum 80 columns × 24 lines recommended
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
### Using pipx
|
|
36
|
+
```bash
|
|
37
|
+
pipx install git+https://github.com/leonst036/Monitorify.git
|
|
38
|
+
```
|
|
39
|
+
Or from PyPI:
|
|
40
|
+
```bash
|
|
41
|
+
pipx install monitorify
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Using pip
|
|
45
|
+
```bash
|
|
46
|
+
pip install git+https://github.com/leonst036/Monitorify.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Using source
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/leonst036/Monitorify.git
|
|
52
|
+
cd Monitorify
|
|
53
|
+
pip install .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
```bash
|
|
58
|
+
monitorify
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Background Daemon
|
|
62
|
+
|
|
63
|
+
The daemon collects metrics continuously to SQLite, even when the dashboard is closed.
|
|
64
|
+
|
|
65
|
+
### Manually (foreground)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
monitorify-daemon
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Always-On via systemd --user
|
|
72
|
+
|
|
73
|
+
Create the service file at `~/.config/systemd/user/monitorify.service`:
|
|
74
|
+
|
|
75
|
+
```ini
|
|
76
|
+
[Unit]
|
|
77
|
+
Description=Monitorify Metrics Collector Daemon
|
|
78
|
+
|
|
79
|
+
[Service]
|
|
80
|
+
ExecStart=%h/.local/bin/monitorify-daemon
|
|
81
|
+
Restart=always
|
|
82
|
+
|
|
83
|
+
[Install]
|
|
84
|
+
WantedBy=default.target
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Enable and start it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
systemctl --user daemon-reload
|
|
91
|
+
systemctl --user enable --now monitorify
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Check status or logs:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
systemctl --user status monitorify
|
|
98
|
+
journalctl --user -u monitorify -f
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Stop or disable:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
systemctl --user stop monitorify
|
|
105
|
+
systemctl --user disable monitorify
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## File Locations
|
|
109
|
+
|
|
110
|
+
Monitorify follows XDG Base Directory conventions — nothing is written to the install directory.
|
|
111
|
+
|
|
112
|
+
| Purpose | Default Path | Override |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| **Database** | `~/.local/share/monitorify/monitorify.db` | `MONITORIFY_DB_PATH` env var |
|
|
115
|
+
| **Daemon PID** | `/run/user/<uid>/monitorify/daemon.pid` | `MONITORIFY_PID_PATH` env var |
|
|
116
|
+
| **Collection interval** | `1.0` seconds | `MONITORIFY_INTERVAL` env var |
|
|
117
|
+
| **Data retention** | `604800` s (7 days) | `MONITORIFY_RETENTION` env var |
|
|
118
|
+
|
|
119
|
+
## Remote
|
|
120
|
+
|
|
121
|
+
To use Monitorify remotely over SSH on your server, you can set the following parameters:
|
|
122
|
+
|
|
123
|
+
- `--ip` – Set the target IP address.
|
|
124
|
+
- `--port` – Set the SSH port.
|
|
125
|
+
- `--username` – Set the username for the remote host.
|
|
126
|
+
- `--password` – Set the password for the SSH server.
|
|
127
|
+
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
128
|
+
- `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
|
|
129
|
+
|
|
130
|
+
## Tech Stack
|
|
131
|
+
|
|
132
|
+
* **Language & TUI:** Python, Textual
|
|
133
|
+
* **Database:** SQLite
|
|
134
|
+
* **Target OS:** Linux
|
|
135
|
+
|
|
136
|
+
## Uninstalling
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# If installed via pipx
|
|
140
|
+
pipx uninstall monitorify
|
|
141
|
+
|
|
142
|
+
# Remove stored data
|
|
143
|
+
rm -rf ~/.local/share/monitorify
|
|
144
|
+
rm -rf /run/user/$(id -u)/monitorify
|
|
145
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Monitorify
|
|
2
|
+
### A terminal montioring tool for linux
|
|
3
|
+
|
|
4
|
+
- Features
|
|
5
|
+
- CPU monitoring
|
|
6
|
+
- RAM monitoring
|
|
7
|
+
- Network monitoring
|
|
8
|
+
- Disk monitoring (I/O and storage capacity)
|
|
9
|
+
- Process monitoring
|
|
10
|
+
- Background daemon
|
|
11
|
+
- Customization menu (toggle widgets, set intervals, and change history window)
|
|
12
|
+
- Historical graphs
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
* **Python:** 3.10+
|
|
17
|
+
* **OS:** Linux (relies on `/proc` and `/sys` filesystems)
|
|
18
|
+
* **Terminal:** Minimum 80 columns × 24 lines recommended
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
### Using pipx
|
|
22
|
+
```bash
|
|
23
|
+
pipx install git+https://github.com/leonst036/Monitorify.git
|
|
24
|
+
```
|
|
25
|
+
Or from PyPI:
|
|
26
|
+
```bash
|
|
27
|
+
pipx install monitorify
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Using pip
|
|
31
|
+
```bash
|
|
32
|
+
pip install git+https://github.com/leonst036/Monitorify.git
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Using source
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/leonst036/Monitorify.git
|
|
38
|
+
cd Monitorify
|
|
39
|
+
pip install .
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
```bash
|
|
44
|
+
monitorify
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Background Daemon
|
|
48
|
+
|
|
49
|
+
The daemon collects metrics continuously to SQLite, even when the dashboard is closed.
|
|
50
|
+
|
|
51
|
+
### Manually (foreground)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
monitorify-daemon
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Always-On via systemd --user
|
|
58
|
+
|
|
59
|
+
Create the service file at `~/.config/systemd/user/monitorify.service`:
|
|
60
|
+
|
|
61
|
+
```ini
|
|
62
|
+
[Unit]
|
|
63
|
+
Description=Monitorify Metrics Collector Daemon
|
|
64
|
+
|
|
65
|
+
[Service]
|
|
66
|
+
ExecStart=%h/.local/bin/monitorify-daemon
|
|
67
|
+
Restart=always
|
|
68
|
+
|
|
69
|
+
[Install]
|
|
70
|
+
WantedBy=default.target
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Enable and start it:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
systemctl --user daemon-reload
|
|
77
|
+
systemctl --user enable --now monitorify
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Check status or logs:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
systemctl --user status monitorify
|
|
84
|
+
journalctl --user -u monitorify -f
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Stop or disable:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
systemctl --user stop monitorify
|
|
91
|
+
systemctl --user disable monitorify
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## File Locations
|
|
95
|
+
|
|
96
|
+
Monitorify follows XDG Base Directory conventions — nothing is written to the install directory.
|
|
97
|
+
|
|
98
|
+
| Purpose | Default Path | Override |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| **Database** | `~/.local/share/monitorify/monitorify.db` | `MONITORIFY_DB_PATH` env var |
|
|
101
|
+
| **Daemon PID** | `/run/user/<uid>/monitorify/daemon.pid` | `MONITORIFY_PID_PATH` env var |
|
|
102
|
+
| **Collection interval** | `1.0` seconds | `MONITORIFY_INTERVAL` env var |
|
|
103
|
+
| **Data retention** | `604800` s (7 days) | `MONITORIFY_RETENTION` env var |
|
|
104
|
+
|
|
105
|
+
## Remote
|
|
106
|
+
|
|
107
|
+
To use Monitorify remotely over SSH on your server, you can set the following parameters:
|
|
108
|
+
|
|
109
|
+
- `--ip` – Set the target IP address.
|
|
110
|
+
- `--port` – Set the SSH port.
|
|
111
|
+
- `--username` – Set the username for the remote host.
|
|
112
|
+
- `--password` – Set the password for the SSH server.
|
|
113
|
+
- `--key_filename` – Set the path to an SSH key *(optional)*.
|
|
114
|
+
- `--host` – Manage remote hosts in the database (`add`, `list`, `remove`).
|
|
115
|
+
|
|
116
|
+
## Tech Stack
|
|
117
|
+
|
|
118
|
+
* **Language & TUI:** Python, Textual
|
|
119
|
+
* **Database:** SQLite
|
|
120
|
+
* **Target OS:** Linux
|
|
121
|
+
|
|
122
|
+
## Uninstalling
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# If installed via pipx
|
|
126
|
+
pipx uninstall monitorify
|
|
127
|
+
|
|
128
|
+
# Remove stored data
|
|
129
|
+
rm -rf ~/.local/share/monitorify
|
|
130
|
+
rm -rf /run/user/$(id -u)/monitorify
|
|
131
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "monitorify"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "A lightweight Linux terminal system monitor built with Textual"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -16,6 +16,8 @@ dependencies = [
|
|
|
16
16
|
"textual>=0.80.0",
|
|
17
17
|
"platformdirs>=4.0.0",
|
|
18
18
|
"rich>=13.0.0",
|
|
19
|
+
"paramiko>=3.0.0",
|
|
20
|
+
"keyring>=24.0.0",
|
|
19
21
|
]
|
|
20
22
|
|
|
21
23
|
[project.scripts]
|
|
@@ -27,3 +29,4 @@ where = ["src"]
|
|
|
27
29
|
|
|
28
30
|
[tool.setuptools.package-data]
|
|
29
31
|
"monitorify.ui.tui" = ["css/*.css"]
|
|
32
|
+
"monitorify.remote" = ["*.sh"]
|
|
@@ -22,7 +22,7 @@ CONFIG_PATH = Path(os.getenv("MONITORIFY_CONFIG_PATH", str(DATA_DIR / "config.js
|
|
|
22
22
|
# Daemon PID file path
|
|
23
23
|
DAEMON_PIDFILE = Path(os.getenv("MONITORIFY_PID_PATH", str(RUNTIME_DIR / "daemon.pid")))
|
|
24
24
|
|
|
25
|
-
# Collection settings
|
|
25
|
+
# Collection settings
|
|
26
26
|
DEFAULT_COLLECTION_INTERVAL = float(os.getenv("MONITORIFY_INTERVAL", "1.0"))
|
|
27
27
|
|
|
28
28
|
# Retention settings (default: 7 days in seconds)
|
|
@@ -13,9 +13,17 @@ def parse_args():
|
|
|
13
13
|
parser.add_argument("--username", type=str, default=None, help="Username for the remote machine")
|
|
14
14
|
parser.add_argument("--password", type=str, default=None, help="Password for the remote machine")
|
|
15
15
|
parser.add_argument("--key_filename", type=str, default=None, help="Path to the key file for the remote machine")
|
|
16
|
+
parser.add_argument("--command", type=str, default=None, help="Command to run on the remote machine")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"--host",
|
|
19
|
+
type=str,
|
|
20
|
+
choices=["add", "list", "remove", "delete"],
|
|
21
|
+
default=None,
|
|
22
|
+
help="Manage remote hosts (e.g. add, list, remove)",
|
|
23
|
+
)
|
|
16
24
|
args = parser.parse_args()
|
|
17
25
|
|
|
18
|
-
if not args.ip and (args.port is not None or args.username or args.password or args.key_filename):
|
|
26
|
+
if not args.ip and not args.host and (args.port is not None or args.username or args.password or args.key_filename):
|
|
19
27
|
parser.error("--ip is required when specifying remote connection arguments")
|
|
20
28
|
|
|
21
29
|
return args
|
|
@@ -50,42 +58,28 @@ def start_daemon() -> subprocess.Popen | None:
|
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
def cli():
|
|
53
|
-
"""Main CLI entrypoint"""
|
|
61
|
+
"""Main CLI entrypoint."""
|
|
54
62
|
args = parse_args()
|
|
55
63
|
|
|
56
|
-
if args.
|
|
57
|
-
from monitorify.remote.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if not success:
|
|
75
|
-
print("Installation failed.")
|
|
76
|
-
sys.exit(1)
|
|
77
|
-
else:
|
|
78
|
-
sys.exit(1)
|
|
79
|
-
|
|
80
|
-
remote_cmd = args.command or "bash -l -c 'monitorify || python3 -m monitorify'"
|
|
81
|
-
connector.run_interactive(client, command=remote_cmd)
|
|
82
|
-
finally:
|
|
83
|
-
client.close()
|
|
84
|
-
except ConnectionError as e:
|
|
85
|
-
print(f"Connection failed: {e}")
|
|
64
|
+
if args.host:
|
|
65
|
+
from monitorify.remote.addRemote import handle_host_action
|
|
66
|
+
|
|
67
|
+
handle_host_action(args.host, args)
|
|
68
|
+
return
|
|
69
|
+
elif args.ip:
|
|
70
|
+
from monitorify.remote.connector import connect_and_run
|
|
71
|
+
|
|
72
|
+
success = connect_and_run(
|
|
73
|
+
host=args.ip,
|
|
74
|
+
port=args.port or 22,
|
|
75
|
+
username=args.username,
|
|
76
|
+
password=args.password,
|
|
77
|
+
key_filename=args.key_filename,
|
|
78
|
+
command=args.command,
|
|
79
|
+
pause_on_exit=False,
|
|
80
|
+
)
|
|
81
|
+
if not success:
|
|
86
82
|
sys.exit(1)
|
|
87
|
-
except KeyboardInterrupt:
|
|
88
|
-
pass
|
|
89
83
|
return
|
|
90
84
|
|
|
91
85
|
# Local mode
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import getpass
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from monitorify.storage.db import Database
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_remote(
|
|
8
|
+
ip: Optional[str] = None,
|
|
9
|
+
port: Optional[int] = None,
|
|
10
|
+
username: Optional[str] = None,
|
|
11
|
+
password: Optional[str] = None,
|
|
12
|
+
key_filename: Optional[str] = None,
|
|
13
|
+
db: Optional[Database] = None,
|
|
14
|
+
) -> int:
|
|
15
|
+
"""Prompt for remote host details if not provided, and save to database."""
|
|
16
|
+
if not ip:
|
|
17
|
+
ip = input("Enter IP address: ").strip()
|
|
18
|
+
if not ip:
|
|
19
|
+
print("Error: IP address cannot be empty.")
|
|
20
|
+
return -1
|
|
21
|
+
|
|
22
|
+
if port is None:
|
|
23
|
+
port_input = input("Enter port (default 22): ").strip()
|
|
24
|
+
port = int(port_input) if port_input.isdigit() else 22
|
|
25
|
+
elif isinstance(port, str):
|
|
26
|
+
port = int(port) if port.isdigit() else 22
|
|
27
|
+
|
|
28
|
+
if not (1 <= port <= 65535):
|
|
29
|
+
port = 22
|
|
30
|
+
|
|
31
|
+
if username is None:
|
|
32
|
+
username_input = input("Enter username (default root): ").strip()
|
|
33
|
+
username = username_input if username_input else "root"
|
|
34
|
+
else:
|
|
35
|
+
username = username.strip() or "root"
|
|
36
|
+
|
|
37
|
+
if password is None and key_filename is None:
|
|
38
|
+
password_input = getpass.getpass("Enter password (leave blank to use SSH keys): ").strip()
|
|
39
|
+
if password_input:
|
|
40
|
+
password = password_input
|
|
41
|
+
key_filename = None
|
|
42
|
+
else:
|
|
43
|
+
password = None
|
|
44
|
+
key_input = input("Enter key filename: ").strip()
|
|
45
|
+
key_filename = key_input if key_input else None
|
|
46
|
+
else:
|
|
47
|
+
password = password if password else None
|
|
48
|
+
key_filename = key_filename if key_filename else None
|
|
49
|
+
|
|
50
|
+
if key_filename:
|
|
51
|
+
key_filename = os.path.expanduser(key_filename)
|
|
52
|
+
|
|
53
|
+
close_db = False
|
|
54
|
+
if db is None:
|
|
55
|
+
db = Database()
|
|
56
|
+
close_db = True
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
host_id = db.save_host(
|
|
60
|
+
ip=ip,
|
|
61
|
+
port=port,
|
|
62
|
+
username=username,
|
|
63
|
+
password=password,
|
|
64
|
+
key_filename=key_filename,
|
|
65
|
+
)
|
|
66
|
+
if host_id != -1:
|
|
67
|
+
print(f"Host '{username}@{ip}:{port}' saved to database (ID: {host_id}).")
|
|
68
|
+
else:
|
|
69
|
+
print("Failed to save host to database.")
|
|
70
|
+
return host_id
|
|
71
|
+
finally:
|
|
72
|
+
if close_db:
|
|
73
|
+
db.disconnect()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def list_hosts(db: Optional[Database] = None) -> list[dict]:
|
|
77
|
+
"""List all saved remote hosts."""
|
|
78
|
+
close_db = False
|
|
79
|
+
if db is None:
|
|
80
|
+
db = Database()
|
|
81
|
+
close_db = True
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
hosts = db.get_hosts()
|
|
85
|
+
if not hosts:
|
|
86
|
+
print("No saved hosts found.")
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
print(f"{'ID':<5} {'Host':<35} {'Auth':<20}")
|
|
90
|
+
print("-" * 65)
|
|
91
|
+
for h in hosts:
|
|
92
|
+
host_str = f"{h['username']}@{h['ip']}:{h['port']}"
|
|
93
|
+
auth_str = f"Key: {h['key_filename']}" if h['key_filename'] else ("Password: ***" if h['password'] else "None")
|
|
94
|
+
print(f"{h['id']:<5} {host_str:<35} {auth_str:<20}")
|
|
95
|
+
return hosts
|
|
96
|
+
finally:
|
|
97
|
+
if close_db:
|
|
98
|
+
db.disconnect()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def remove_host(host_id: Optional[int] = None, db: Optional[Database] = None) -> bool:
|
|
102
|
+
"""Remove a saved remote host by ID."""
|
|
103
|
+
close_db = False
|
|
104
|
+
if db is None:
|
|
105
|
+
db = Database()
|
|
106
|
+
close_db = True
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
if host_id is None:
|
|
110
|
+
hosts = db.get_hosts()
|
|
111
|
+
if not hosts:
|
|
112
|
+
print("No saved hosts to remove.")
|
|
113
|
+
return False
|
|
114
|
+
list_hosts(db=db)
|
|
115
|
+
id_input = input("Enter host ID to remove: ").strip()
|
|
116
|
+
if not id_input.isdigit():
|
|
117
|
+
print("Invalid host ID.")
|
|
118
|
+
return False
|
|
119
|
+
host_id = int(id_input)
|
|
120
|
+
|
|
121
|
+
if db.delete_host(host_id):
|
|
122
|
+
print(f"Host ID {host_id} successfully deleted.")
|
|
123
|
+
return True
|
|
124
|
+
else:
|
|
125
|
+
print(f"Failed to delete host ID {host_id}.")
|
|
126
|
+
return False
|
|
127
|
+
finally:
|
|
128
|
+
if close_db:
|
|
129
|
+
db.disconnect()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def handle_host_action(action: str, args=None) -> None:
|
|
133
|
+
"""Handle --host subcommands."""
|
|
134
|
+
action = (action or "").lower()
|
|
135
|
+
if action == "add":
|
|
136
|
+
ip = getattr(args, "ip", None)
|
|
137
|
+
port = getattr(args, "port", None)
|
|
138
|
+
username = getattr(args, "username", None)
|
|
139
|
+
password = getattr(args, "password", None)
|
|
140
|
+
key_filename = getattr(args, "key_filename", None)
|
|
141
|
+
add_remote(
|
|
142
|
+
ip=ip,
|
|
143
|
+
port=port,
|
|
144
|
+
username=username,
|
|
145
|
+
password=password,
|
|
146
|
+
key_filename=key_filename,
|
|
147
|
+
)
|
|
148
|
+
elif action == "list":
|
|
149
|
+
list_hosts()
|
|
150
|
+
elif action in ("remove", "delete"):
|
|
151
|
+
remove_host()
|
|
152
|
+
else:
|
|
153
|
+
print(f"Unknown host action: '{action}'. Supported actions: add, list, remove")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
add_remote()
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import paramiko
|
|
2
3
|
|
|
4
|
+
|
|
3
5
|
class RemoteConnector:
|
|
4
6
|
def __init__(
|
|
5
7
|
self,
|
|
@@ -8,18 +10,27 @@ class RemoteConnector:
|
|
|
8
10
|
username: str | None = None,
|
|
9
11
|
password: str | None = None,
|
|
10
12
|
key_filename: str | None = None,
|
|
13
|
+
timeout: float = 10.0,
|
|
11
14
|
):
|
|
12
15
|
self.host = host
|
|
13
16
|
self.port = port
|
|
14
17
|
self.username = username
|
|
15
18
|
self.password = password
|
|
16
|
-
self.key_filename = key_filename
|
|
19
|
+
self.key_filename = os.path.expanduser(key_filename) if key_filename else None
|
|
20
|
+
self.timeout = timeout
|
|
17
21
|
|
|
18
|
-
def connect(self):
|
|
22
|
+
def connect(self) -> paramiko.SSHClient:
|
|
19
23
|
client = paramiko.SSHClient()
|
|
20
24
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
21
|
-
try:
|
|
22
|
-
client.connect(
|
|
25
|
+
try:
|
|
26
|
+
client.connect(
|
|
27
|
+
hostname=self.host,
|
|
28
|
+
port=self.port,
|
|
29
|
+
username=self.username,
|
|
30
|
+
password=self.password,
|
|
31
|
+
key_filename=self.key_filename,
|
|
32
|
+
timeout=self.timeout,
|
|
33
|
+
)
|
|
23
34
|
except paramiko.AuthenticationException as e:
|
|
24
35
|
raise ConnectionError(f"Authentication failed: {e}")
|
|
25
36
|
except paramiko.SSHException as e:
|
|
@@ -32,7 +43,7 @@ class RemoteConnector:
|
|
|
32
43
|
"""Check if monitorify is installed on the remote host."""
|
|
33
44
|
try:
|
|
34
45
|
_, stdout, _ = client.exec_command(
|
|
35
|
-
"bash -l -c 'command -v monitorify >/dev/null 2>&1 || python3 -c \"import monitorify\" >/dev/null 2>&1'"
|
|
46
|
+
"bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; command -v monitorify >/dev/null 2>&1 || [ -x \"$HOME/.local/bin/monitorify\" ] || python3 -c \"import monitorify\" >/dev/null 2>&1'"
|
|
36
47
|
)
|
|
37
48
|
return stdout.channel.recv_exit_status() == 0
|
|
38
49
|
except Exception:
|
|
@@ -51,7 +62,6 @@ class RemoteConnector:
|
|
|
51
62
|
|
|
52
63
|
def run_interactive(self, client: paramiko.SSHClient, command: str = "monitorify") -> None:
|
|
53
64
|
"""Run a command interactively on the remote host with a PTY."""
|
|
54
|
-
import os
|
|
55
65
|
import select
|
|
56
66
|
import shutil
|
|
57
67
|
import signal
|
|
@@ -81,8 +91,9 @@ class RemoteConnector:
|
|
|
81
91
|
except Exception:
|
|
82
92
|
pass
|
|
83
93
|
|
|
94
|
+
old_winch_handler = None
|
|
84
95
|
try:
|
|
85
|
-
signal.signal(signal.SIGWINCH, handle_resize)
|
|
96
|
+
old_winch_handler = signal.signal(signal.SIGWINCH, handle_resize)
|
|
86
97
|
except Exception:
|
|
87
98
|
pass
|
|
88
99
|
|
|
@@ -110,6 +121,69 @@ class RemoteConnector:
|
|
|
110
121
|
except Exception:
|
|
111
122
|
break
|
|
112
123
|
finally:
|
|
124
|
+
if old_winch_handler is not None:
|
|
125
|
+
try:
|
|
126
|
+
signal.signal(signal.SIGWINCH, old_winch_handler)
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
113
129
|
if is_atty and old_tty is not None:
|
|
114
130
|
termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_tty)
|
|
115
|
-
channel.close()
|
|
131
|
+
channel.close()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def connect_and_run(
|
|
135
|
+
host: str,
|
|
136
|
+
port: int = 22,
|
|
137
|
+
username: str | None = None,
|
|
138
|
+
password: str | None = None,
|
|
139
|
+
key_filename: str | None = None,
|
|
140
|
+
command: str | None = None,
|
|
141
|
+
pause_on_exit: bool = False,
|
|
142
|
+
timeout: float = 10.0,
|
|
143
|
+
) -> bool:
|
|
144
|
+
"""Connects to a remote host and runs the interactive session."""
|
|
145
|
+
connector = RemoteConnector(
|
|
146
|
+
host=host,
|
|
147
|
+
port=port or 22,
|
|
148
|
+
username=username,
|
|
149
|
+
password=password,
|
|
150
|
+
key_filename=key_filename,
|
|
151
|
+
timeout=timeout,
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
print(f"\nConnecting to {username or 'root'}@{host}:{port or 22}...")
|
|
155
|
+
client = connector.connect()
|
|
156
|
+
try:
|
|
157
|
+
if not command and not connector.is_installed(client):
|
|
158
|
+
print(f"Error: Monitorify is not installed on the remote host ({host}).")
|
|
159
|
+
response = input("Do you want to install it? (y/n): ").strip().lower()
|
|
160
|
+
if response == "y":
|
|
161
|
+
success = connector.install(client)
|
|
162
|
+
if not success:
|
|
163
|
+
print("Installation failed.")
|
|
164
|
+
if pause_on_exit:
|
|
165
|
+
input("Press Enter to return...")
|
|
166
|
+
return False
|
|
167
|
+
else:
|
|
168
|
+
if pause_on_exit:
|
|
169
|
+
input("Press Enter to return...")
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
remote_cmd = command or "bash -l -c 'export PATH=\"$HOME/.local/bin:$PATH\"; monitorify || python3 -m monitorify'"
|
|
173
|
+
connector.run_interactive(client, command=remote_cmd)
|
|
174
|
+
return True
|
|
175
|
+
finally:
|
|
176
|
+
client.close()
|
|
177
|
+
except ConnectionError as e:
|
|
178
|
+
print(f"Connection failed: {e}")
|
|
179
|
+
if pause_on_exit:
|
|
180
|
+
input("Press Enter to return...")
|
|
181
|
+
return False
|
|
182
|
+
except KeyboardInterrupt:
|
|
183
|
+
print("\nSession cancelled.")
|
|
184
|
+
return False
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Unexpected error: {e}")
|
|
187
|
+
if pause_on_exit:
|
|
188
|
+
input("Press Enter to return...")
|
|
189
|
+
return False
|