msdev 0.15.0__tar.gz → 0.15.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.
- {msdev-0.15.0 → msdev-0.15.1}/PKG-INFO +13 -6
- {msdev-0.15.0 → msdev-0.15.1}/README.md +12 -5
- {msdev-0.15.0 → msdev-0.15.1}/pyproject.toml +1 -4
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/__init__.py +1 -1
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/cli.py +54 -29
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/config.py +0 -17
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/guides.py +12 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/inventory.py +1 -1
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/limits.py +19 -1
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/resources.py +50 -6
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/context.py +1 -6
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/environment.py +5 -6
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/node.py +94 -36
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/ssh_config.py +42 -0
- msdev-0.15.1/src/msdev/core/transport.py +1021 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/skills/msdev-cli/SKILL.md +20 -5
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/PKG-INFO +13 -6
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/SOURCES.txt +2 -36
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/entry_points.txt +0 -1
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_cli.py +49 -16
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_cli_only_architecture.py +1 -1
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_compat.py +0 -12
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_config.py +0 -18
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_transfer.py +5 -6
- msdev-0.15.1/tests/test_live_dev122_dev186_scenario.py +196 -0
- msdev-0.15.1/tests/test_local_docker_ssh_smoke_scenario.py +1078 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_bootstrap_version_scenario.py +2 -3
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_connect_provision_scenario.py +1 -1
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_connect_save_password_scenario.py +1 -1
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_env_config.py +65 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_ssh_auth.py +9 -63
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_target_exec_services.py +70 -22
- msdev-0.15.1/tests/test_transport.py +379 -0
- {msdev-0.15.0 → msdev-0.15.1}/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.1}/LICENSE +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/setup.cfg +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/__init__.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/compat.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/direct_exec.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/env_exec.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/model.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/services/results.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/sftp_transfer.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_config_bundle.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_connect_env_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_exec_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_argv.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_command_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_exec_default_timeout_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_init_skill.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_inventory.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_model_cli.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_model_services.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_msdev_cli_skill.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_env_services.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_sftp_transfer_scenario.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_ssh_config.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/tests/test_target_guides.py +0 -0
- {msdev-0.15.0 → msdev-0.15.1}/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.1
|
|
4
4
|
Summary: Portable nodes and execution environments for msModelSlim development
|
|
5
5
|
Author: msModelSlim Agent Contributors
|
|
6
6
|
License-Expression: MulanPSL-2.0
|
|
@@ -115,13 +115,20 @@ Connect atomically creates a same-named host Env on first use. There is no
|
|
|
115
115
|
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
116
116
|
re-check SSH readiness after connect problems.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Inspect connectivity for one node or for every registered node (probes run in
|
|
119
|
+
parallel; partial failures exit non-zero):
|
|
119
120
|
|
|
120
121
|
```bash
|
|
121
|
-
|
|
122
|
+
msdev node status
|
|
123
|
+
msdev node status dev-122
|
|
122
124
|
```
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
Rename a node together with its same-named host Env, guide files, and SSH
|
|
127
|
+
`Host` alias:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
msdev node rename dev-122 lab-122
|
|
131
|
+
```
|
|
125
132
|
|
|
126
133
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
127
134
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -171,11 +178,11 @@ msdev env add dev-122-root \
|
|
|
171
178
|
msdev env open dev-122-root
|
|
172
179
|
```
|
|
173
180
|
|
|
174
|
-
Open
|
|
181
|
+
Open an interactive shell in an env. Docker envs enter the container:
|
|
175
182
|
|
|
176
183
|
```bash
|
|
177
184
|
msdev env shell dev-122
|
|
178
|
-
msdev env shell dev-122 --cwd /srv/project
|
|
185
|
+
msdev env shell dev-122-docker --cwd /srv/project
|
|
179
186
|
```
|
|
180
187
|
|
|
181
188
|
Agent automation should use `msdev exec`, not `env shell`.
|
|
@@ -90,13 +90,20 @@ Connect atomically creates a same-named host Env on first use. There is no
|
|
|
90
90
|
remote agent binary to deploy. Use **`msdev node bootstrap NODE`** only to
|
|
91
91
|
re-check SSH readiness after connect problems.
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Inspect connectivity for one node or for every registered node (probes run in
|
|
94
|
+
parallel; partial failures exit non-zero):
|
|
94
95
|
|
|
95
96
|
```bash
|
|
96
|
-
|
|
97
|
+
msdev node status
|
|
98
|
+
msdev node status dev-122
|
|
97
99
|
```
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
Rename a node together with its same-named host Env, guide files, and SSH
|
|
102
|
+
`Host` alias:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
msdev node rename dev-122 lab-122
|
|
106
|
+
```
|
|
100
107
|
|
|
101
108
|
Persistent OpenSSH masters are optional. All SSH from msdev is
|
|
102
109
|
non-interactive: host keys use `accept-new`, and password/passphrase prompts
|
|
@@ -146,11 +153,11 @@ msdev env add dev-122-root \
|
|
|
146
153
|
msdev env open dev-122-root
|
|
147
154
|
```
|
|
148
155
|
|
|
149
|
-
Open
|
|
156
|
+
Open an interactive shell in an env. Docker envs enter the container:
|
|
150
157
|
|
|
151
158
|
```bash
|
|
152
159
|
msdev env shell dev-122
|
|
153
|
-
msdev env shell dev-122 --cwd /srv/project
|
|
160
|
+
msdev env shell dev-122-docker --cwd /srv/project
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
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.1"
|
|
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}"
|
|
@@ -615,19 +636,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
615
636
|
action="store_true",
|
|
616
637
|
help="also remove every environment on this node",
|
|
617
638
|
)
|
|
618
|
-
|
|
619
|
-
|
|
639
|
+
node_rename = node_sub.add_parser(
|
|
640
|
+
"rename",
|
|
641
|
+
help="rename a node, its host environment, and a matching SSH Host alias",
|
|
642
|
+
)
|
|
643
|
+
node_rename.add_argument("name")
|
|
644
|
+
node_rename.add_argument("new_name")
|
|
645
|
+
node_status = node_sub.add_parser(
|
|
646
|
+
"status",
|
|
647
|
+
help="check node connectivity (every registered SSH node by default)",
|
|
648
|
+
)
|
|
649
|
+
node_status.add_argument(
|
|
650
|
+
"name",
|
|
651
|
+
nargs="?",
|
|
652
|
+
help="check one node; omit to check every registered SSH node",
|
|
653
|
+
)
|
|
620
654
|
node_bootstrap = node_sub.add_parser(
|
|
621
655
|
"bootstrap",
|
|
622
656
|
help="ensure SSH reachability / ControlMaster for the node",
|
|
623
657
|
)
|
|
624
658
|
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
659
|
node_connect = node_sub.add_parser(
|
|
632
660
|
"connect",
|
|
633
661
|
help=(
|
|
@@ -659,17 +687,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
659
687
|
"--ssh-host",
|
|
660
688
|
help="legacy shorthand for user@hostname when writing ~/.ssh/config",
|
|
661
689
|
)
|
|
662
|
-
node_connect.add_argument("--remote-bin", default="~/.local/bin/msdevd")
|
|
663
690
|
node_connect.add_argument(
|
|
664
691
|
"--force",
|
|
665
692
|
action="store_true",
|
|
666
693
|
help="replace an existing node and its same-named host environment",
|
|
667
694
|
)
|
|
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
695
|
node_connect.add_argument(
|
|
674
696
|
"--password",
|
|
675
697
|
default=None,
|
|
@@ -749,7 +771,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
749
771
|
environment_inspect.add_argument("name")
|
|
750
772
|
environment_open = environment_sub.add_parser(
|
|
751
773
|
"open",
|
|
752
|
-
help="
|
|
774
|
+
help="check that the environment is ready (setup runs on each exec)",
|
|
753
775
|
)
|
|
754
776
|
environment_open.add_argument("name")
|
|
755
777
|
environment_guide = environment_sub.add_parser("guide")
|
|
@@ -757,7 +779,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
757
779
|
environment_guide_action = environment_guide.add_mutually_exclusive_group()
|
|
758
780
|
environment_guide_action.add_argument("--write", metavar="FILE")
|
|
759
781
|
environment_guide_action.add_argument("--delete", action="store_true")
|
|
760
|
-
environment_shell = environment_sub.add_parser(
|
|
782
|
+
environment_shell = environment_sub.add_parser(
|
|
783
|
+
"shell",
|
|
784
|
+
help="open an interactive shell in the environment (including Docker)",
|
|
785
|
+
)
|
|
761
786
|
environment_shell.add_argument("name")
|
|
762
787
|
environment_shell.add_argument("--cwd")
|
|
763
788
|
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:
|