mrg-iot 1.1.8__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.
- mrg_iot-1.1.8/LICENSE +21 -0
- mrg_iot-1.1.8/PKG-INFO +165 -0
- mrg_iot-1.1.8/README.md +103 -0
- mrg_iot-1.1.8/grpc_runner.py +93 -0
- mrg_iot-1.1.8/log_manager.py +53 -0
- mrg_iot-1.1.8/miko_tunnel.py +145 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/PKG-INFO +165 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/SOURCES.txt +24 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/dependency_links.txt +1 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/entry_points.txt +2 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/requires.txt +33 -0
- mrg_iot-1.1.8/mrg_iot.egg-info/top_level.txt +7 -0
- mrg_iot-1.1.8/mrg_version +1 -0
- mrg_iot-1.1.8/mrgiot.py +323 -0
- mrg_iot-1.1.8/mrgiot_api_calls.py +764 -0
- mrg_iot-1.1.8/mrgiot_consts.py +171 -0
- mrg_iot-1.1.8/mrgiot_runner.py +1202 -0
- mrg_iot-1.1.8/pyproject.toml +90 -0
- mrg_iot-1.1.8/setup.cfg +4 -0
- mrg_iot-1.1.8/setup.py +65 -0
- mrg_iot-1.1.8/tests/test_cli.py +287 -0
- mrg_iot-1.1.8/tests/test_dataclasses.py +83 -0
- mrg_iot-1.1.8/tests/test_duration.py +129 -0
- mrg_iot-1.1.8/tests/test_read_message.py +101 -0
- mrg_iot-1.1.8/tests/test_runner_helpers.py +157 -0
- mrg_iot-1.1.8/tests/test_validation.py +225 -0
mrg_iot-1.1.8/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sphere / NEU IoT Testbed contributors
|
|
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.
|
mrg_iot-1.1.8/PKG-INFO
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mrg-iot
|
|
3
|
+
Version: 1.1.8
|
|
4
|
+
Summary: IoT Testbed Experiment Automation CLI for MergeTB / Sphere Testbed
|
|
5
|
+
Author: Sphere / NEU IoT Testbed contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/sphere-neu/mrg-iot
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/sphere-neu/mrg-iot
|
|
9
|
+
Project-URL: Issues, https://gitlab.com/sphere-neu/mrg-iot/-/issues
|
|
10
|
+
Keywords: iot,testbed,mergetb,sphere,automation,cli
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: bcrypt>=5.0.0
|
|
29
|
+
Requires-Dist: betterproto==2.0.0b7
|
|
30
|
+
Requires-Dist: certifi
|
|
31
|
+
Requires-Dist: cffi>=2.0.0
|
|
32
|
+
Requires-Dist: cryptography>=46.0.3
|
|
33
|
+
Requires-Dist: grpclib>=0.4.8
|
|
34
|
+
Requires-Dist: h2>=4.3.0
|
|
35
|
+
Requires-Dist: hpack>=4.1.0
|
|
36
|
+
Requires-Dist: hyperframe>=6.1.0
|
|
37
|
+
Requires-Dist: mergetbapi==1.3.41
|
|
38
|
+
Requires-Dist: multidict>=6.7.0
|
|
39
|
+
Requires-Dist: packaging>=25.0
|
|
40
|
+
Requires-Dist: paramiko>=4.0.0
|
|
41
|
+
Requires-Dist: pycparser>=2.23
|
|
42
|
+
Requires-Dist: PyNaCl>=1.6.0
|
|
43
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
44
|
+
Requires-Dist: six>=1.17.0
|
|
45
|
+
Requires-Dist: typing_extensions>=4.15.0
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: astroid==4.0.1; extra == "dev"
|
|
48
|
+
Requires-Dist: black==25.9.0; extra == "dev"
|
|
49
|
+
Requires-Dist: dill==0.4.0; extra == "dev"
|
|
50
|
+
Requires-Dist: invoke==2.2.1; extra == "dev"
|
|
51
|
+
Requires-Dist: isort==7.0.0; extra == "dev"
|
|
52
|
+
Requires-Dist: mccabe==0.7.0; extra == "dev"
|
|
53
|
+
Requires-Dist: mypy_extensions==1.1.0; extra == "dev"
|
|
54
|
+
Requires-Dist: pathspec==0.12.1; extra == "dev"
|
|
55
|
+
Requires-Dist: platformdirs==4.5.0; extra == "dev"
|
|
56
|
+
Requires-Dist: pylint==4.0.2; extra == "dev"
|
|
57
|
+
Requires-Dist: pytokens==0.2.0; extra == "dev"
|
|
58
|
+
Requires-Dist: tomlkit==0.13.3; extra == "dev"
|
|
59
|
+
Requires-Dist: types-paramiko; extra == "dev"
|
|
60
|
+
Dynamic: license-file
|
|
61
|
+
Dynamic: requires-python
|
|
62
|
+
|
|
63
|
+
# mrg-iot
|
|
64
|
+
|
|
65
|
+
A Python CLI for automating IoT testbed experiments on the
|
|
66
|
+
[Sphere / Merge Testbed](https://sphere-testbed.net) platform.
|
|
67
|
+
|
|
68
|
+
`mrg-iot` drives the full experiment lifecycle so you don't have to:
|
|
69
|
+
|
|
70
|
+
- Create an experiment and its network model for the selected devices.
|
|
71
|
+
- Realize and materialize the model.
|
|
72
|
+
- Create an XDC (eXperimental Data Center) and attach it to the
|
|
73
|
+
materialization.
|
|
74
|
+
- Set up SSH port forwarding for RTSP streams and file downloads.
|
|
75
|
+
- (Optional) De-materialize and clean up resources when you're done.
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pipx install mrg-iot
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(Or `pip install mrg-iot` inside a virtualenv.)
|
|
84
|
+
|
|
85
|
+
`mrg-iot` requires **Python 3.9+** and a working
|
|
86
|
+
[VLC](https://www.videolan.org/vlc/) install on the host for RTSP
|
|
87
|
+
playback — `pip` can't install VLC for you.
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### Interactive mode
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mrg-iot login # save credentials to ~/.mrg-iot/config.json
|
|
95
|
+
mrg-iot # walk through the experiment flow
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Non-interactive mode
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
mrg-iot --non-interactive \
|
|
102
|
+
--project neuiot \
|
|
103
|
+
--devices s-echodot-1 \
|
|
104
|
+
--exp-name myexp \
|
|
105
|
+
--exp-desc "my experiment" \
|
|
106
|
+
--realization realiot \
|
|
107
|
+
--duration 1w \
|
|
108
|
+
--xdc myxdc
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cleanup subcommands
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
mrg-iot exp delete --project neuiot --name myexp
|
|
115
|
+
mrg-iot exp dematerialize --project neuiot --name myexp --realization realiot
|
|
116
|
+
mrg-iot xdc detach --project neuiot --name myxdc --experiment myexp --realization realiot
|
|
117
|
+
mrg-iot xdc delete --project neuiot --name myxdc
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Run `mrg-iot --help` for the full flag list.
|
|
121
|
+
|
|
122
|
+
### Debug logging
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
mrg-iot --debug # verbose logs to stderr + debug.log
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Input validation
|
|
129
|
+
|
|
130
|
+
The CLI validates inputs upfront so bad args fail fast rather than
|
|
131
|
+
deep inside a portal call:
|
|
132
|
+
|
|
133
|
+
- **Names** (`exp-name`, `realization`, `xdc`, network): start with a
|
|
134
|
+
letter, lowercase letters and digits only, max 32 chars.
|
|
135
|
+
- **Description**: letters, digits, spaces, commas, periods, hyphens;
|
|
136
|
+
max 256 chars.
|
|
137
|
+
- **Duration**: minimum 4 days. Accepts `1w`, `4d`, `1w2d`,
|
|
138
|
+
`1 week`, etc.
|
|
139
|
+
|
|
140
|
+
## macOS SSL note
|
|
141
|
+
|
|
142
|
+
If portal calls fail with an SSL error on macOS:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
export SSL_CERT_FILE="$(python -m certifi)"
|
|
146
|
+
export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
git clone https://gitlab.com/sphere-neu/mrg-iot.git
|
|
153
|
+
cd mrg-iot
|
|
154
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
155
|
+
pip install -e ".[dev]"
|
|
156
|
+
|
|
157
|
+
mypy .
|
|
158
|
+
pylint *.py
|
|
159
|
+
black *.py
|
|
160
|
+
isort *.py
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT — see [LICENSE](LICENSE).
|
mrg_iot-1.1.8/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# mrg-iot
|
|
2
|
+
|
|
3
|
+
A Python CLI for automating IoT testbed experiments on the
|
|
4
|
+
[Sphere / Merge Testbed](https://sphere-testbed.net) platform.
|
|
5
|
+
|
|
6
|
+
`mrg-iot` drives the full experiment lifecycle so you don't have to:
|
|
7
|
+
|
|
8
|
+
- Create an experiment and its network model for the selected devices.
|
|
9
|
+
- Realize and materialize the model.
|
|
10
|
+
- Create an XDC (eXperimental Data Center) and attach it to the
|
|
11
|
+
materialization.
|
|
12
|
+
- Set up SSH port forwarding for RTSP streams and file downloads.
|
|
13
|
+
- (Optional) De-materialize and clean up resources when you're done.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pipx install mrg-iot
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
(Or `pip install mrg-iot` inside a virtualenv.)
|
|
22
|
+
|
|
23
|
+
`mrg-iot` requires **Python 3.9+** and a working
|
|
24
|
+
[VLC](https://www.videolan.org/vlc/) install on the host for RTSP
|
|
25
|
+
playback — `pip` can't install VLC for you.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Interactive mode
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
mrg-iot login # save credentials to ~/.mrg-iot/config.json
|
|
33
|
+
mrg-iot # walk through the experiment flow
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Non-interactive mode
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mrg-iot --non-interactive \
|
|
40
|
+
--project neuiot \
|
|
41
|
+
--devices s-echodot-1 \
|
|
42
|
+
--exp-name myexp \
|
|
43
|
+
--exp-desc "my experiment" \
|
|
44
|
+
--realization realiot \
|
|
45
|
+
--duration 1w \
|
|
46
|
+
--xdc myxdc
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Cleanup subcommands
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
mrg-iot exp delete --project neuiot --name myexp
|
|
53
|
+
mrg-iot exp dematerialize --project neuiot --name myexp --realization realiot
|
|
54
|
+
mrg-iot xdc detach --project neuiot --name myxdc --experiment myexp --realization realiot
|
|
55
|
+
mrg-iot xdc delete --project neuiot --name myxdc
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run `mrg-iot --help` for the full flag list.
|
|
59
|
+
|
|
60
|
+
### Debug logging
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mrg-iot --debug # verbose logs to stderr + debug.log
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Input validation
|
|
67
|
+
|
|
68
|
+
The CLI validates inputs upfront so bad args fail fast rather than
|
|
69
|
+
deep inside a portal call:
|
|
70
|
+
|
|
71
|
+
- **Names** (`exp-name`, `realization`, `xdc`, network): start with a
|
|
72
|
+
letter, lowercase letters and digits only, max 32 chars.
|
|
73
|
+
- **Description**: letters, digits, spaces, commas, periods, hyphens;
|
|
74
|
+
max 256 chars.
|
|
75
|
+
- **Duration**: minimum 4 days. Accepts `1w`, `4d`, `1w2d`,
|
|
76
|
+
`1 week`, etc.
|
|
77
|
+
|
|
78
|
+
## macOS SSL note
|
|
79
|
+
|
|
80
|
+
If portal calls fail with an SSL error on macOS:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export SSL_CERT_FILE="$(python -m certifi)"
|
|
84
|
+
export REQUESTS_CA_BUNDLE="$SSL_CERT_FILE"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://gitlab.com/sphere-neu/mrg-iot.git
|
|
91
|
+
cd mrg-iot
|
|
92
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
|
|
95
|
+
mypy .
|
|
96
|
+
pylint *.py
|
|
97
|
+
black *.py
|
|
98
|
+
isort *.py
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Creates an implementation for easily managing a grpc call and accounting for stream terminations
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import socket
|
|
6
|
+
from typing import Any
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from types import FunctionType
|
|
10
|
+
import betterproto
|
|
11
|
+
from grpclib import GRPCError
|
|
12
|
+
from grpclib.client import Channel
|
|
13
|
+
from grpclib.config import Configuration
|
|
14
|
+
from grpclib.exceptions import StreamTerminatedError
|
|
15
|
+
|
|
16
|
+
GRPC_HOST = "grpc.sphere-testbed.net"
|
|
17
|
+
GRPC_PORT = 443
|
|
18
|
+
MAX_TERM_RETRIES = 3
|
|
19
|
+
AUTH = "authorization"
|
|
20
|
+
TOKEN_TEMPLATE = "Bearer {}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GrpcRunner:
|
|
24
|
+
"""
|
|
25
|
+
Class for running grpc calls and handling stream terminations
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
async def run(
|
|
30
|
+
grpc_stub: type[betterproto.ServiceStub],
|
|
31
|
+
target: FunctionType,
|
|
32
|
+
request: betterproto.Message,
|
|
33
|
+
token: str = "",
|
|
34
|
+
) -> Any:
|
|
35
|
+
"""
|
|
36
|
+
Runs a grpc method with a given stub and target function
|
|
37
|
+
|
|
38
|
+
:param grpc_stub: The stub to call the function from
|
|
39
|
+
:param target: The target function to call
|
|
40
|
+
:param request: The request object of the function
|
|
41
|
+
:param token: User's authentication token, defaults to ""
|
|
42
|
+
:raises GRPCError: When the grpc call raises an exception back
|
|
43
|
+
:raises StreamTerminatedError: When the stream terminates due to idle
|
|
44
|
+
:return: Response from the grpc function
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
i = MAX_TERM_RETRIES
|
|
48
|
+
|
|
49
|
+
while i > 0:
|
|
50
|
+
|
|
51
|
+
channel = GrpcRunner.create_channel()
|
|
52
|
+
stub = grpc_stub(channel)
|
|
53
|
+
meta_data = {AUTH: TOKEN_TEMPLATE.format(token)} if token else None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
# pyright: ignore[reportCallIssue]
|
|
57
|
+
response: betterproto.Message = await target(
|
|
58
|
+
stub, request, metadata=meta_data
|
|
59
|
+
)
|
|
60
|
+
channel.close()
|
|
61
|
+
return response
|
|
62
|
+
|
|
63
|
+
except GRPCError as e:
|
|
64
|
+
channel.close()
|
|
65
|
+
logging.debug("GRPC channel closed after error")
|
|
66
|
+
raise e
|
|
67
|
+
|
|
68
|
+
except (StreamTerminatedError, socket.gaierror) as e:
|
|
69
|
+
i -= 1
|
|
70
|
+
|
|
71
|
+
if i <= 0:
|
|
72
|
+
print(
|
|
73
|
+
"[mrg-iot] "
|
|
74
|
+
+ "Communication with Merge Portal interrupted and failed to reconnect. "
|
|
75
|
+
"Please wait a few minutes before before attempting communication again. "
|
|
76
|
+
f"Received from communication channel: {e}"
|
|
77
|
+
)
|
|
78
|
+
logging.exception("Max retries exceeded | original_error=%s", e)
|
|
79
|
+
raise e
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def create_channel() -> Channel:
|
|
83
|
+
"""
|
|
84
|
+
Creates a channel for use for grpc communication
|
|
85
|
+
|
|
86
|
+
:return: grpc channel connected to
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
config = Configuration(30, 15, True)
|
|
90
|
+
|
|
91
|
+
channel = Channel(GRPC_HOST, GRPC_PORT, ssl=True, config=config)
|
|
92
|
+
|
|
93
|
+
return channel
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A module to hold all logging-related classes and methods.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from mrgiot_consts import DEBUG_LOG_PATH
|
|
7
|
+
|
|
8
|
+
class LogManager:
|
|
9
|
+
"""
|
|
10
|
+
Singleton class for managing logging configuration.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
_instance = None
|
|
14
|
+
_initialized = False
|
|
15
|
+
|
|
16
|
+
def __new__(cls):
|
|
17
|
+
if cls._instance is None:
|
|
18
|
+
cls._instance = super().__new__(cls)
|
|
19
|
+
return cls._instance
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
if self._initialized:
|
|
23
|
+
return
|
|
24
|
+
self._initialized = True
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def get_instance() -> "LogManager":
|
|
28
|
+
"""This function returns the instance of our Log Manager
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
LogManager: The instance of our Log Manager
|
|
32
|
+
"""
|
|
33
|
+
if LogManager._instance is None:
|
|
34
|
+
return LogManager()
|
|
35
|
+
return LogManager._instance
|
|
36
|
+
|
|
37
|
+
def configure_logging(self, debug: bool = False):
|
|
38
|
+
"""This function configures logging
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
debug (bool, optional): handles debug logging based on the debug flag. Defaults to False.
|
|
42
|
+
"""
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
filename=DEBUG_LOG_PATH,
|
|
45
|
+
level=logging.DEBUG if debug else logging.INFO,
|
|
46
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
47
|
+
datefmt="%d %B %Y %I:%M:%S %p",
|
|
48
|
+
filemode="w",
|
|
49
|
+
)
|
|
50
|
+
logging.getLogger("paramiko").setLevel(logging.WARNING)
|
|
51
|
+
logging.getLogger("paramiko.transport").setLevel(logging.WARNING)
|
|
52
|
+
logging.getLogger("hpack").setLevel(logging.WARNING)
|
|
53
|
+
logging.getLogger("grpclib").setLevel(logging.WARNING)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation for a paramiko based tunnel based on information gotten from the following demo:
|
|
3
|
+
https://github.com/paramiko/paramiko/blob/main/demos/forward.py
|
|
4
|
+
|
|
5
|
+
Which creates a substitution for an openSSH tunnel since it is not implementated by paramiko
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from socketserver import ThreadingTCPServer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import select
|
|
12
|
+
import socketserver
|
|
13
|
+
import threading
|
|
14
|
+
import paramiko
|
|
15
|
+
from paramiko.ssh_exception import SSHException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# pylint: disable=too-few-public-methods
|
|
19
|
+
class MikoTunnel:
|
|
20
|
+
"""
|
|
21
|
+
Implementation for a paramiko based tunnel
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__transport: paramiko.Transport
|
|
25
|
+
__remote_host: str
|
|
26
|
+
__remote_port: int
|
|
27
|
+
__local_port: int
|
|
28
|
+
__forward_server: socketserver.ThreadingTCPServer
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
transport: paramiko.Transport,
|
|
33
|
+
remote_host: str,
|
|
34
|
+
remote_port: int,
|
|
35
|
+
local_port: int = -1,
|
|
36
|
+
):
|
|
37
|
+
"""
|
|
38
|
+
Instantiates a paramiko based forwarding tunnel
|
|
39
|
+
|
|
40
|
+
:param transport: paramiko transport to be the source of the tunnel
|
|
41
|
+
:param remote_host: The remote host to bind to
|
|
42
|
+
:param remote_port: The remote port to bind to
|
|
43
|
+
:param local_port: The local port to bind to, defaults to match remote_port
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
self.transport = transport
|
|
47
|
+
self.remote_host = remote_host
|
|
48
|
+
self.remote_port = remote_port
|
|
49
|
+
self.local_port = local_port if local_port > 0 else remote_port
|
|
50
|
+
self.__forward_server = self.__start()
|
|
51
|
+
|
|
52
|
+
def __start(self) -> ThreadingTCPServer:
|
|
53
|
+
"""
|
|
54
|
+
Starts the tunnel by opening a TCP server that forwards traffic as a daemon thread
|
|
55
|
+
|
|
56
|
+
:return: The running traffic forwarding server
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
class _Handler(ForwardHandler):
|
|
60
|
+
"""
|
|
61
|
+
Local subclass for passing information about the remote host to the server
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
target_host = self.remote_host
|
|
65
|
+
target_port = self.remote_port
|
|
66
|
+
ssh_transport = self.transport
|
|
67
|
+
|
|
68
|
+
tunnel = socketserver.ThreadingTCPServer(("", self.local_port), _Handler)
|
|
69
|
+
tunnel.allow_reuse_address = True
|
|
70
|
+
tunnel.daemon_threads = True
|
|
71
|
+
thread = threading.Thread(target=tunnel.serve_forever, daemon=True)
|
|
72
|
+
thread.start()
|
|
73
|
+
|
|
74
|
+
return tunnel
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""
|
|
78
|
+
Translates a close call into the server close call to properly close the tunnel
|
|
79
|
+
"""
|
|
80
|
+
self.__forward_server.server_close()
|
|
81
|
+
|
|
82
|
+
def __del__(self):
|
|
83
|
+
"""
|
|
84
|
+
Automatically closes the tunnel when the instance is deleted to prevent sockets from being occupied
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
self.close()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ForwardHandler(socketserver.BaseRequestHandler):
|
|
92
|
+
"""
|
|
93
|
+
Handler for the tunnel that forwards traffic from each end of the tunnel to the other
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
target_host: str
|
|
97
|
+
target_port: int
|
|
98
|
+
ssh_transport: paramiko.Transport
|
|
99
|
+
|
|
100
|
+
def handle(self) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Handler function for handling incoming communication from the either end of the tunnel
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
channel = self.ssh_transport.open_channel(
|
|
107
|
+
"direct-tcpip",
|
|
108
|
+
(self.target_host, self.target_port),
|
|
109
|
+
self.request.getpeername(),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
except SSHException as e:
|
|
113
|
+
print(e)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
if not channel:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# Main tunnel loop
|
|
120
|
+
try:
|
|
121
|
+
while not channel.closed:
|
|
122
|
+
read_list, _, _ = select.select([self.request, channel], [], [], 1.0)
|
|
123
|
+
|
|
124
|
+
# Send incoming traffic out
|
|
125
|
+
if self.request in read_list:
|
|
126
|
+
traffic = self.request.recv(1024)
|
|
127
|
+
if not traffic:
|
|
128
|
+
break
|
|
129
|
+
channel.send(traffic)
|
|
130
|
+
|
|
131
|
+
# Recieve or redirect inbound traffic
|
|
132
|
+
if channel in read_list:
|
|
133
|
+
traffic = channel.recv(1024)
|
|
134
|
+
if not traffic:
|
|
135
|
+
break
|
|
136
|
+
self.request.send(traffic)
|
|
137
|
+
|
|
138
|
+
except (ConnectionResetError, OSError):
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
finally:
|
|
142
|
+
|
|
143
|
+
# Close the socket if needed
|
|
144
|
+
if self.request.fileno() != -1:
|
|
145
|
+
self.request.close()
|