msdev 0.15.0__tar.gz → 0.15.2__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.
- {msdev-0.15.0 → msdev-0.15.2}/PKG-INFO +14 -6
- {msdev-0.15.0 → msdev-0.15.2}/README.md +13 -5
- {msdev-0.15.0 → msdev-0.15.2}/pyproject.toml +1 -4
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/__init__.py +1 -1
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/cli.py +60 -29
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/config.py +0 -17
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/guides.py +12 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/inventory.py +1 -1
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/limits.py +19 -1
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/resources.py +50 -6
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/context.py +1 -6
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/environment.py +5 -6
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/node.py +94 -36
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/ssh_config.py +42 -0
- msdev-0.15.2/src/msdev/core/transport.py +1021 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/skills/msdev-cli/SKILL.md +20 -5
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/PKG-INFO +14 -6
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/SOURCES.txt +3 -36
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/entry_points.txt +0 -1
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_cli.py +59 -16
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_cli_only_architecture.py +1 -1
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_compat.py +0 -12
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_config.py +0 -18
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_transfer.py +5 -6
- msdev-0.15.2/tests/test_live_dev122_dev186_scenario.py +196 -0
- msdev-0.15.2/tests/test_local_docker_ssh_smoke_scenario.py +1078 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_bootstrap_version_scenario.py +2 -3
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_connect_provision_scenario.py +1 -1
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_connect_save_password_scenario.py +1 -1
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_env_config.py +65 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_ssh_auth.py +9 -63
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_target_exec_services.py +70 -22
- msdev-0.15.2/tests/test_transport.py +379 -0
- msdev-0.15.2/tests/test_version_scenario.py +47 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_windows_local_scenario.py +2 -19
- msdev-0.15.0/src/msdev/core/container_helper.py +0 -251
- msdev-0.15.0/src/msdev/core/daemon_deploy.py +0 -49
- msdev-0.15.0/src/msdev/core/daemon_version.py +0 -26
- msdev-0.15.0/src/msdev/core/helper_deploy.py +0 -143
- msdev-0.15.0/src/msdev/core/host_files.py +0 -92
- msdev-0.15.0/src/msdev/core/host_mutations.py +0 -332
- msdev-0.15.0/src/msdev/core/local_rpc.py +0 -85
- msdev-0.15.0/src/msdev/core/multiplex.py +0 -143
- msdev-0.15.0/src/msdev/core/request_client.py +0 -174
- msdev-0.15.0/src/msdev/core/ssh_tunnel.py +0 -561
- msdev-0.15.0/src/msdev/core/stream_limits.py +0 -17
- msdev-0.15.0/src/msdev/core/stream_server.py +0 -606
- msdev-0.15.0/src/msdev/core/stream_transport.py +0 -376
- msdev-0.15.0/src/msdev/core/streamproto.py +0 -129
- msdev-0.15.0/src/msdev/core/transport.py +0 -1498
- msdev-0.15.0/src/msdev/daemon.py +0 -1499
- msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -1
- msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -1
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64 +0 -0
- msdev-0.15.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
- msdev-0.15.0/tests/test_container_helper.py +0 -234
- msdev-0.15.0/tests/test_daemon.py +0 -1092
- msdev-0.15.0/tests/test_daemon_container_stream.py +0 -84
- msdev-0.15.0/tests/test_daemon_deploy.py +0 -150
- msdev-0.15.0/tests/test_docker_stream_dispatch.py +0 -237
- msdev-0.15.0/tests/test_go_msdevd_scenario.py +0 -209
- msdev-0.15.0/tests/test_helper_deploy.py +0 -185
- msdev-0.15.0/tests/test_multiplex.py +0 -199
- msdev-0.15.0/tests/test_request_client.py +0 -241
- msdev-0.15.0/tests/test_ssh_tunnel.py +0 -57
- msdev-0.15.0/tests/test_stream_server.py +0 -510
- msdev-0.15.0/tests/test_stream_transport.py +0 -314
- msdev-0.15.0/tests/test_streamproto.py +0 -65
- msdev-0.15.0/tests/test_transport.py +0 -1252
- {msdev-0.15.0 → msdev-0.15.2}/LICENSE +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/setup.cfg +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/__init__.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/compat.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/direct_exec.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/env_exec.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/model.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/services/results.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/sftp_transfer.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_config_bundle.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_connect_env_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_exec_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_argv.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_command_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_exec_default_timeout_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_init_skill.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_inventory.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_model_cli.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_model_services.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_msdev_cli_skill.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_env_services.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_sftp_transfer_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_ssh_config.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_target_guides.py +0 -0
- {msdev-0.15.0 → msdev-0.15.2}/tests/test_windows_git_bash_ssh_scenario.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msdev
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.2
|
|
4
4
|
Summary: Portable nodes and execution environments for msModelSlim development
|
|
5
5
|
Author: msModelSlim Agent Contributors
|
|
6
6
|
License-Expression: MulanPSL-2.0
|
|
@@ -44,6 +44,7 @@ git clone https://gitcode.com/rookie_hongchuan/msdev.git
|
|
|
44
44
|
cd msdev
|
|
45
45
|
python3 -m pip install -e .
|
|
46
46
|
msdev --help
|
|
47
|
+
msdev --version
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
The package installs the `msdev` CLI.
|
|
@@ -115,13 +116,20 @@ Connect atomically creates a same-named host Env on first use. There is no
|
|
|
115
116
|
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
116
117
|
re-check SSH readiness after connect problems.
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
Inspect connectivity for one node or for every registered node (probes run in
|
|
120
|
+
parallel; partial failures exit non-zero):
|
|
119
121
|
|
|
120
122
|
```bash
|
|
121
|
-
|
|
123
|
+
msdev node status
|
|
124
|
+
msdev node status dev-122
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
Rename a node together with its same-named host Env, guide files, and SSH
|
|
128
|
+
`Host` alias:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
msdev node rename dev-122 lab-122
|
|
132
|
+
```
|
|
125
133
|
|
|
126
134
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
127
135
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -171,11 +179,11 @@ msdev env add dev-122-root \
|
|
|
171
179
|
msdev env open dev-122-root
|
|
172
180
|
```
|
|
173
181
|
|
|
174
|
-
Open
|
|
182
|
+
Open an interactive shell in an env. Docker envs enter the container:
|
|
175
183
|
|
|
176
184
|
```bash
|
|
177
185
|
msdev env shell dev-122
|
|
178
|
-
msdev env shell dev-122 --cwd /srv/project
|
|
186
|
+
msdev env shell dev-122-docker --cwd /srv/project
|
|
179
187
|
```
|
|
180
188
|
|
|
181
189
|
Agent automation should use `msdev exec`, not `env shell`.
|
|
@@ -19,6 +19,7 @@ git clone https://gitcode.com/rookie_hongchuan/msdev.git
|
|
|
19
19
|
cd msdev
|
|
20
20
|
python3 -m pip install -e .
|
|
21
21
|
msdev --help
|
|
22
|
+
msdev --version
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
The package installs the `msdev` CLI.
|
|
@@ -90,13 +91,20 @@ Connect atomically creates a same-named host Env on first use. There is no
|
|
|
90
91
|
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
91
92
|
re-check SSH readiness after connect problems.
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
Inspect connectivity for one node or for every registered node (probes run in
|
|
95
|
+
parallel; partial failures exit non-zero):
|
|
94
96
|
|
|
95
97
|
```bash
|
|
96
|
-
|
|
98
|
+
msdev node status
|
|
99
|
+
msdev node status dev-122
|
|
97
100
|
```
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
Rename a node together with its same-named host Env, guide files, and SSH
|
|
103
|
+
`Host` alias:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
msdev node rename dev-122 lab-122
|
|
107
|
+
```
|
|
100
108
|
|
|
101
109
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
102
110
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -146,11 +154,11 @@ msdev env add dev-122-root \
|
|
|
146
154
|
msdev env open dev-122-root
|
|
147
155
|
```
|
|
148
156
|
|
|
149
|
-
Open
|
|
157
|
+
Open an interactive shell in an env. Docker envs enter the container:
|
|
150
158
|
|
|
151
159
|
```bash
|
|
152
160
|
msdev env shell dev-122
|
|
153
|
-
msdev env shell dev-122 --cwd /srv/project
|
|
161
|
+
msdev env shell dev-122-docker --cwd /srv/project
|
|
154
162
|
```
|
|
155
163
|
|
|
156
164
|
Agent automation should use `msdev exec`, not `env shell`.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "msdev"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.2"
|
|
8
8
|
description = "Portable nodes and execution environments for msModelSlim development"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -35,7 +35,6 @@ Repository = "https://gitcode.com/rookie_hongchuan/msdev"
|
|
|
35
35
|
|
|
36
36
|
[project.scripts]
|
|
37
37
|
msdev = "msdev.cli:main"
|
|
38
|
-
msdevd = "msdev.daemon:main"
|
|
39
38
|
|
|
40
39
|
[tool.setuptools]
|
|
41
40
|
package-dir = { "" = "src" }
|
|
@@ -45,8 +44,6 @@ where = ["src"]
|
|
|
45
44
|
|
|
46
45
|
[tool.setuptools.package-data]
|
|
47
46
|
msdev = [
|
|
48
|
-
"helpers/msdev-file-helper-*",
|
|
49
|
-
"helpers/msdevd-linux-*",
|
|
50
47
|
"skills/msdev-cli/SKILL.md",
|
|
51
48
|
]
|
|
52
49
|
|
|
@@ -154,8 +154,6 @@ def _provision_request_for_connect(
|
|
|
154
154
|
if ssh_host_defined(args.name):
|
|
155
155
|
return NodeAddRequest(
|
|
156
156
|
name=args.name,
|
|
157
|
-
remote_bin=args.remote_bin,
|
|
158
|
-
auto_bootstrap=not args.no_auto_bootstrap,
|
|
159
157
|
force=args.force,
|
|
160
158
|
)
|
|
161
159
|
raise ValueError(
|
|
@@ -169,8 +167,6 @@ def _provision_request_for_connect(
|
|
|
169
167
|
port=args.port,
|
|
170
168
|
identity_file=args.identity_file,
|
|
171
169
|
ssh_host=args.ssh_host,
|
|
172
|
-
remote_bin=args.remote_bin,
|
|
173
|
-
auto_bootstrap=not args.no_auto_bootstrap,
|
|
174
170
|
force=bool(
|
|
175
171
|
args.force or has_params
|
|
176
172
|
),
|
|
@@ -204,9 +200,7 @@ def command_node(
|
|
|
204
200
|
print("local\tregistered\t-")
|
|
205
201
|
for node in service.list().nodes:
|
|
206
202
|
print(
|
|
207
|
-
f"{node.name}\tregistered\tssh={node.ssh_host}
|
|
208
|
-
f"auto-bootstrap={'on' if node.auto_bootstrap else 'off'}\t"
|
|
209
|
-
f"bin={node.remote_bin}"
|
|
203
|
+
f"{node.name}\tregistered\tssh={node.ssh_host}"
|
|
210
204
|
)
|
|
211
205
|
if args.ssh:
|
|
212
206
|
for host in service.list_ssh_hosts():
|
|
@@ -236,12 +230,39 @@ def command_node(
|
|
|
236
230
|
if removed:
|
|
237
231
|
print("removed environments: " + ", ".join(removed))
|
|
238
232
|
return 0
|
|
239
|
-
if action == "
|
|
240
|
-
|
|
241
|
-
|
|
233
|
+
if action == "rename":
|
|
234
|
+
node = service.rename(args.name, args.new_name)
|
|
235
|
+
print(f"renamed node {args.name} -> {node.name} (ssh={node.ssh_host})")
|
|
242
236
|
return 0
|
|
237
|
+
if action == "status":
|
|
238
|
+
if args.name:
|
|
239
|
+
print(f"node: {args.name}")
|
|
240
|
+
_print_json(service.status(args.name))
|
|
241
|
+
return 0
|
|
242
|
+
result = service.status_all()
|
|
243
|
+
for item in result.items:
|
|
244
|
+
if item.error is not None:
|
|
245
|
+
print(
|
|
246
|
+
f"{item.node} ({item.ssh_host}): unreachable: {item.error}",
|
|
247
|
+
file=sys.stderr,
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
info = item.info if isinstance(item.info, dict) else {}
|
|
251
|
+
parts: list[str] = []
|
|
252
|
+
hostname = str(info.get("hostname") or "")
|
|
253
|
+
ip = str(info.get("ip") or "")
|
|
254
|
+
target = str(info.get("target") or "")
|
|
255
|
+
if hostname:
|
|
256
|
+
parts.append(f"hostname={hostname}")
|
|
257
|
+
if ip:
|
|
258
|
+
parts.append(f"ip={ip}")
|
|
259
|
+
if target and target not in {hostname, ip, item.ssh_host}:
|
|
260
|
+
parts.append(f"target={target}")
|
|
261
|
+
detail = (" " + " ".join(parts)) if parts else ""
|
|
262
|
+
print(f"{item.node} ({item.ssh_host}): reachable{detail}")
|
|
263
|
+
return result.exit_code
|
|
243
264
|
if action == "bootstrap":
|
|
244
|
-
result = service.bootstrap(args.name
|
|
265
|
+
result = service.bootstrap(args.name)
|
|
245
266
|
_print_json(result)
|
|
246
267
|
return 0
|
|
247
268
|
if action in {"connect", "disconnect"}:
|
|
@@ -281,8 +302,8 @@ def command_node(
|
|
|
281
302
|
key = getattr(item, "key", None)
|
|
282
303
|
if key:
|
|
283
304
|
suffix = f"(persist={item.persist}, key={key})"
|
|
284
|
-
if item.
|
|
285
|
-
suffix = suffix[:-1] + ",
|
|
305
|
+
if item.ssh_ready:
|
|
306
|
+
suffix = suffix[:-1] + ", ssh=ready)"
|
|
286
307
|
print(
|
|
287
308
|
f"{item.node}: {item.state} to {item.ssh_host} "
|
|
288
309
|
f"{suffix}"
|
|
@@ -573,6 +594,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
573
594
|
default=None,
|
|
574
595
|
help="override local node/environment registry path",
|
|
575
596
|
)
|
|
597
|
+
parser.add_argument(
|
|
598
|
+
"--version",
|
|
599
|
+
action="version",
|
|
600
|
+
version=f"%(prog)s {__version__}",
|
|
601
|
+
help="print the msdev version and exit",
|
|
602
|
+
)
|
|
576
603
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
577
604
|
|
|
578
605
|
node = sub.add_parser("node", help="manage SSH connection nodes")
|
|
@@ -615,19 +642,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
615
642
|
action="store_true",
|
|
616
643
|
help="also remove every environment on this node",
|
|
617
644
|
)
|
|
618
|
-
|
|
619
|
-
|
|
645
|
+
node_rename = node_sub.add_parser(
|
|
646
|
+
"rename",
|
|
647
|
+
help="rename a node, its host environment, and a matching SSH Host alias",
|
|
648
|
+
)
|
|
649
|
+
node_rename.add_argument("name")
|
|
650
|
+
node_rename.add_argument("new_name")
|
|
651
|
+
node_status = node_sub.add_parser(
|
|
652
|
+
"status",
|
|
653
|
+
help="check node connectivity (every registered SSH node by default)",
|
|
654
|
+
)
|
|
655
|
+
node_status.add_argument(
|
|
656
|
+
"name",
|
|
657
|
+
nargs="?",
|
|
658
|
+
help="check one node; omit to check every registered SSH node",
|
|
659
|
+
)
|
|
620
660
|
node_bootstrap = node_sub.add_parser(
|
|
621
661
|
"bootstrap",
|
|
622
662
|
help="ensure SSH reachability / ControlMaster for the node",
|
|
623
663
|
)
|
|
624
664
|
node_bootstrap.add_argument("name")
|
|
625
|
-
node_bootstrap.add_argument(
|
|
626
|
-
"--package",
|
|
627
|
-
type=Path,
|
|
628
|
-
default=None,
|
|
629
|
-
help="ignored (kept for compatibility; remote msdevd is no longer deployed)",
|
|
630
|
-
)
|
|
631
665
|
node_connect = node_sub.add_parser(
|
|
632
666
|
"connect",
|
|
633
667
|
help=(
|
|
@@ -659,17 +693,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
659
693
|
"--ssh-host",
|
|
660
694
|
help="legacy shorthand for user@hostname when writing ~/.ssh/config",
|
|
661
695
|
)
|
|
662
|
-
node_connect.add_argument("--remote-bin", default="~/.local/bin/msdevd")
|
|
663
696
|
node_connect.add_argument(
|
|
664
697
|
"--force",
|
|
665
698
|
action="store_true",
|
|
666
699
|
help="replace an existing node and its same-named host environment",
|
|
667
700
|
)
|
|
668
|
-
node_connect.add_argument(
|
|
669
|
-
"--no-auto-bootstrap",
|
|
670
|
-
action="store_true",
|
|
671
|
-
help="ignored (kept for compatibility; no remote agent deploy)",
|
|
672
|
-
)
|
|
673
701
|
node_connect.add_argument(
|
|
674
702
|
"--password",
|
|
675
703
|
default=None,
|
|
@@ -749,7 +777,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
749
777
|
environment_inspect.add_argument("name")
|
|
750
778
|
environment_open = environment_sub.add_parser(
|
|
751
779
|
"open",
|
|
752
|
-
help="
|
|
780
|
+
help="check that the environment is ready (setup runs on each exec)",
|
|
753
781
|
)
|
|
754
782
|
environment_open.add_argument("name")
|
|
755
783
|
environment_guide = environment_sub.add_parser("guide")
|
|
@@ -757,7 +785,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
757
785
|
environment_guide_action = environment_guide.add_mutually_exclusive_group()
|
|
758
786
|
environment_guide_action.add_argument("--write", metavar="FILE")
|
|
759
787
|
environment_guide_action.add_argument("--delete", action="store_true")
|
|
760
|
-
environment_shell = environment_sub.add_parser(
|
|
788
|
+
environment_shell = environment_sub.add_parser(
|
|
789
|
+
"shell",
|
|
790
|
+
help="open an interactive shell in the environment (including Docker)",
|
|
791
|
+
)
|
|
761
792
|
environment_shell.add_argument("name")
|
|
762
793
|
environment_shell.add_argument("--cwd")
|
|
763
794
|
environment_put = environment_sub.add_parser(
|
|
@@ -354,20 +354,3 @@ def config_home() -> Path:
|
|
|
354
354
|
return Path(override).expanduser()
|
|
355
355
|
base = os.environ.get("XDG_CONFIG_HOME")
|
|
356
356
|
return Path(base).expanduser() / "msdev" if base else Path.home() / ".config" / "msdev"
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def data_home() -> Path:
|
|
360
|
-
override = os.environ.get("MSDEV_DATA_HOME")
|
|
361
|
-
if override:
|
|
362
|
-
return Path(override).expanduser()
|
|
363
|
-
base = os.environ.get("XDG_DATA_HOME")
|
|
364
|
-
return Path(base).expanduser() / "msdevd" if base else Path.home() / ".local" / "share" / "msdevd"
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
def default_socket_path() -> Path:
|
|
368
|
-
override = os.environ.get("MSDEVD_SOCKET")
|
|
369
|
-
if override:
|
|
370
|
-
return Path(override).expanduser()
|
|
371
|
-
# SSH sessions frequently lack XDG_RUNTIME_DIR while systemd user services
|
|
372
|
-
# set it to /run/user/<uid>. A stable per-user path keeps both sides aligned.
|
|
373
|
-
return data_home() / "msdevd.sock"
|
|
@@ -89,6 +89,18 @@ class ResourceGuideStore:
|
|
|
89
89
|
except FileNotFoundError:
|
|
90
90
|
return False
|
|
91
91
|
|
|
92
|
+
def rename(self, old: str, new: str) -> bool:
|
|
93
|
+
"""Move a guide file. Return whether one existed."""
|
|
94
|
+
source = self.path(old)
|
|
95
|
+
if not source.exists():
|
|
96
|
+
return False
|
|
97
|
+
destination = self.path(new)
|
|
98
|
+
if destination.exists():
|
|
99
|
+
raise ValueError(f"guide already exists for {new!r}")
|
|
100
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
os.replace(source, destination)
|
|
102
|
+
return True
|
|
103
|
+
|
|
92
104
|
|
|
93
105
|
class NodeGuideStore(ResourceGuideStore):
|
|
94
106
|
def __init__(self, root: Path | None = None):
|
|
@@ -10,9 +10,15 @@ MAX_RPC_RESPONSE_BYTES = 32 * 1024 * 1024
|
|
|
10
10
|
MAX_RPC_ERROR_BYTES = 1 * 1024 * 1024
|
|
11
11
|
|
|
12
12
|
DEFAULT_EXEC_TIMEOUT_SECONDS = -1.0
|
|
13
|
-
EXEC_RPC_COMPLETION_GRACE_SECONDS = 10.0
|
|
14
13
|
RPC_PROCESS_CLEANUP_GRACE_SECONDS = 2.0
|
|
15
14
|
|
|
15
|
+
# Infrastructure budget for ControlMaster probe / discard / (re)connect.
|
|
16
|
+
# Separate from exec command deadline (DEFAULT_EXEC_TIMEOUT_SECONDS = -1).
|
|
17
|
+
DEFAULT_SSH_SETUP_TIMEOUT_SECONDS = 15.0
|
|
18
|
+
DEFAULT_SSH_MUX_PROBE_TIMEOUT_SECONDS = 5.0
|
|
19
|
+
# Per-node budget for ``msdev node status`` (parallel; no ControlMaster setup).
|
|
20
|
+
DEFAULT_SSH_STATUS_TIMEOUT_SECONDS = 3.0
|
|
21
|
+
|
|
16
22
|
|
|
17
23
|
def validate_exec_timeout_seconds(value: Any) -> float:
|
|
18
24
|
if (
|
|
@@ -26,6 +32,18 @@ def validate_exec_timeout_seconds(value: Any) -> float:
|
|
|
26
32
|
)
|
|
27
33
|
return float(value)
|
|
28
34
|
|
|
35
|
+
|
|
36
|
+
def validate_positive_timeout_seconds(value: Any, *, label: str) -> float:
|
|
37
|
+
if (
|
|
38
|
+
isinstance(value, bool)
|
|
39
|
+
or not isinstance(value, (int, float))
|
|
40
|
+
or not math.isfinite(value)
|
|
41
|
+
or value <= 0
|
|
42
|
+
):
|
|
43
|
+
raise ValueError(f"{label} must be a positive finite number")
|
|
44
|
+
return float(value)
|
|
45
|
+
|
|
46
|
+
|
|
29
47
|
# Budget for conservative six-byte JSON escaping:
|
|
30
48
|
# 8 MiB content * 6 + 64 maps * 2 paths * 4 KiB * 6 = 51 MiB.
|
|
31
49
|
# The remaining 13 MiB covers the JSON-RPC envelope and structural overhead.
|
|
@@ -21,19 +21,17 @@ def _required(value: str, label: str) -> str:
|
|
|
21
21
|
return value
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
_NODE_LEGACY_FIELDS = frozenset({"remote_bin", "auto_bootstrap"})
|
|
25
|
+
|
|
26
|
+
|
|
24
27
|
@dataclass(frozen=True)
|
|
25
28
|
class Node:
|
|
26
29
|
name: str
|
|
27
30
|
ssh_host: str
|
|
28
|
-
remote_bin: str = "~/.local/bin/msdevd"
|
|
29
|
-
auto_bootstrap: bool = True
|
|
30
31
|
|
|
31
32
|
def __post_init__(self) -> None:
|
|
32
33
|
_required(self.name, "node name")
|
|
33
34
|
_required(self.ssh_host, "SSH host")
|
|
34
|
-
_required(self.remote_bin, "remote binary")
|
|
35
|
-
if type(self.auto_bootstrap) is not bool:
|
|
36
|
-
raise ValueError("auto_bootstrap must be a boolean")
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
@dataclass(frozen=True)
|
|
@@ -124,7 +122,12 @@ class _ResourceStore:
|
|
|
124
122
|
environments: dict[str, dict[str, Any]] = {}
|
|
125
123
|
try:
|
|
126
124
|
for name, raw in value["nodes"].items():
|
|
127
|
-
|
|
125
|
+
node_raw = {
|
|
126
|
+
key: item
|
|
127
|
+
for key, item in dict(raw).items()
|
|
128
|
+
if key not in _NODE_LEGACY_FIELDS
|
|
129
|
+
}
|
|
130
|
+
node = Node(**node_raw)
|
|
128
131
|
if node.name != name:
|
|
129
132
|
raise ValueError(f"node key {name!r} does not match its name")
|
|
130
133
|
nodes[name] = asdict(node)
|
|
@@ -248,6 +251,47 @@ class NodeStore(_ResourceStore):
|
|
|
248
251
|
self._save(self._normalize_saved(value))
|
|
249
252
|
return references
|
|
250
253
|
|
|
254
|
+
def rename(self, old: str, new: str) -> Node:
|
|
255
|
+
"""Rename a node and retarget environments that reference it.
|
|
256
|
+
|
|
257
|
+
The same-named host environment is renamed with the node. ``ssh_host``
|
|
258
|
+
is updated only when it was the old node name.
|
|
259
|
+
"""
|
|
260
|
+
if old == "local" or new == "local":
|
|
261
|
+
raise ValueError("node name 'local' is reserved")
|
|
262
|
+
_required(old, "node name")
|
|
263
|
+
_required(new, "node name")
|
|
264
|
+
if old == new:
|
|
265
|
+
return self.get(old)
|
|
266
|
+
with _exclusive_file_lock(self.lock_path):
|
|
267
|
+
value = self._load_unlocked()
|
|
268
|
+
if old not in value["nodes"]:
|
|
269
|
+
raise KeyError(f"unknown node: {old}")
|
|
270
|
+
if new in value["nodes"]:
|
|
271
|
+
raise ValueError(f"node {new!r} already exists")
|
|
272
|
+
environments = value["environments"]
|
|
273
|
+
if old in environments and new in environments:
|
|
274
|
+
raise ValueError(f"environment {new!r} already exists")
|
|
275
|
+
raw = dict(value["nodes"].pop(old))
|
|
276
|
+
ssh_host = raw["ssh_host"]
|
|
277
|
+
if ssh_host == old:
|
|
278
|
+
ssh_host = new
|
|
279
|
+
node = Node(name=new, ssh_host=ssh_host)
|
|
280
|
+
value["nodes"][new] = asdict(node)
|
|
281
|
+
rewritten: dict[str, Any] = {}
|
|
282
|
+
for name, environment in environments.items():
|
|
283
|
+
updated = dict(environment)
|
|
284
|
+
if updated.get("node") == old:
|
|
285
|
+
updated["node"] = new
|
|
286
|
+
if name == old:
|
|
287
|
+
updated["name"] = new
|
|
288
|
+
rewritten[new] = updated
|
|
289
|
+
else:
|
|
290
|
+
rewritten[name] = updated
|
|
291
|
+
value["environments"] = rewritten
|
|
292
|
+
self._save(self._normalize_saved(value))
|
|
293
|
+
return node
|
|
294
|
+
|
|
251
295
|
|
|
252
296
|
class EnvironmentStore(_ResourceStore):
|
|
253
297
|
def add(self, environment: Environment, *, force: bool = False) -> None:
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass, field
|
|
6
|
-
from pathlib import Path
|
|
7
6
|
from typing import Any, Protocol
|
|
8
7
|
|
|
9
8
|
from ..resources import Environment, EnvironmentStore, Node, NodeStore
|
|
@@ -24,11 +23,7 @@ class TransportFactory(Protocol):
|
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
class BootstrapNode(Protocol):
|
|
27
|
-
def __call__(
|
|
28
|
-
self,
|
|
29
|
-
node: Node,
|
|
30
|
-
package_path: Path | None = None,
|
|
31
|
-
) -> dict[str, Any]: ...
|
|
26
|
+
def __call__(self, node: Node) -> dict[str, Any]: ...
|
|
32
27
|
|
|
33
28
|
|
|
34
29
|
def create_default_transport(
|
|
@@ -189,13 +189,12 @@ class EnvironmentService:
|
|
|
189
189
|
def open(self, name: str) -> dict[str, Any]:
|
|
190
190
|
environment = self._environment(name)
|
|
191
191
|
transport = self.context.transport_for_environment(environment.name)
|
|
192
|
-
|
|
193
|
-
if
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
result = transport.call("env.status", {"_env": environment.to_agent_spec()})
|
|
192
|
+
connect = getattr(transport, "connect", None)
|
|
193
|
+
if connect is not None:
|
|
194
|
+
connect()
|
|
195
|
+
result = transport.call("env.open", {"_env": environment.to_agent_spec()})
|
|
197
196
|
if not isinstance(result, dict):
|
|
198
|
-
raise RuntimeError("env.
|
|
197
|
+
raise RuntimeError("env.open returned a non-object result")
|
|
199
198
|
return result
|
|
200
199
|
|
|
201
200
|
def remove(self, name: str) -> str:
|