msdev 0.13.0__tar.gz → 0.14.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.13.0 → msdev-0.14.1}/PKG-INFO +5 -3
- {msdev-0.13.0 → msdev-0.14.1}/README.md +4 -2
- {msdev-0.13.0 → msdev-0.14.1}/pyproject.toml +1 -1
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/__init__.py +1 -1
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/cli.py +18 -0
- msdev-0.14.1/src/msdev/core/daemon_version.py +26 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/node.py +38 -14
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/sftp_transfer.py +45 -9
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/transport.py +73 -34
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdevd-linux-amd64 +0 -0
- msdev-0.14.1/src/msdev/helpers/msdevd-linux-amd64.sha256 +1 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdevd-linux-arm64 +0 -0
- msdev-0.14.1/src/msdev/helpers/msdevd-linux-arm64.sha256 +1 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/skills/msdev-cli/SKILL.md +8 -2
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev.egg-info/PKG-INFO +5 -3
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev.egg-info/SOURCES.txt +3 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_cli.py +21 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_cli_only_architecture.py +7 -2
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_config_bundle.py +3 -3
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_daemon.py +1 -1
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_go_msdevd_scenario.py +1 -1
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_msdev_cli_skill.py +2 -1
- msdev-0.14.1/tests/test_node_bootstrap_version_scenario.py +174 -0
- msdev-0.14.1/tests/test_sftp_transfer_scenario.py +239 -0
- msdev-0.14.1/tests/test_stale_daemon_autobootstrap_scenario.py +100 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_target_exec_services.py +68 -9
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_transport.py +4 -0
- msdev-0.13.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
- msdev-0.13.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
- msdev-0.13.0/tests/test_sftp_transfer_scenario.py +0 -109
- {msdev-0.13.0 → msdev-0.14.1}/LICENSE +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/setup.cfg +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/__init__.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/compat.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/container_helper.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/daemon_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/guides.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/helper_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/host_files.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/host_mutations.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/inventory.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/limits.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/local_rpc.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/multiplex.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/request_client.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/resources.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/context.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/environment.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/model.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/services/results.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/ssh_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/ssh_tunnel.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/stream_limits.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/stream_server.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/stream_transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/core/streamproto.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/daemon.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev.egg-info/entry_points.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_compat.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_connect_env_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_container_helper.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_daemon_container_stream.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_daemon_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_docker_stream_dispatch.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_env_transfer.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_exec_argv.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_exec_command_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_exec_default_timeout_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_helper_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_init_skill.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_inventory.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_model_cli.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_model_services.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_multiplex.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_node_connect_provision_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_node_connect_save_password_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_node_env_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_node_env_services.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_request_client.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_ssh_auth.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_ssh_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_ssh_tunnel.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_stream_server.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_stream_transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_streamproto.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_target_guides.py +0 -0
- {msdev-0.13.0 → msdev-0.14.1}/tests/test_windows_local_scenario.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msdev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -104,6 +104,7 @@ key, and deploys `msdevd`:
|
|
|
104
104
|
```bash
|
|
105
105
|
msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
|
|
106
106
|
msdev node connect dev-122
|
|
107
|
+
msdev node bootstrap dev-122
|
|
107
108
|
msdev node sync-ssh
|
|
108
109
|
msdev node list --ssh
|
|
109
110
|
msdev node list
|
|
@@ -116,8 +117,9 @@ config. `node sync-ssh` registers Host aliases that were added manually.
|
|
|
116
117
|
|
|
117
118
|
Connect atomically creates a same-named host Env on first use and uploads a
|
|
118
119
|
**static Go msdevd** binary matching the node architecture
|
|
119
|
-
(`linux-amd64` / `linux-arm64`) when the agent is missing or
|
|
120
|
-
|
|
120
|
+
(`linux-amd64` / `linux-arm64`) when the agent is missing or when the remote
|
|
121
|
+
`ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
|
|
122
|
+
to force a redeploy after upgrading msdev. No Python is required on the node.
|
|
121
123
|
|
|
122
124
|
Build the static daemons before release or local testing:
|
|
123
125
|
|
|
@@ -79,6 +79,7 @@ key, and deploys `msdevd`:
|
|
|
79
79
|
```bash
|
|
80
80
|
msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
|
|
81
81
|
msdev node connect dev-122
|
|
82
|
+
msdev node bootstrap dev-122
|
|
82
83
|
msdev node sync-ssh
|
|
83
84
|
msdev node list --ssh
|
|
84
85
|
msdev node list
|
|
@@ -91,8 +92,9 @@ config. `node sync-ssh` registers Host aliases that were added manually.
|
|
|
91
92
|
|
|
92
93
|
Connect atomically creates a same-named host Env on first use and uploads a
|
|
93
94
|
**static Go msdevd** binary matching the node architecture
|
|
94
|
-
(`linux-amd64` / `linux-arm64`) when the agent is missing or
|
|
95
|
-
|
|
95
|
+
(`linux-amd64` / `linux-arm64`) when the agent is missing or when the remote
|
|
96
|
+
`ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
|
|
97
|
+
to force a redeploy after upgrading msdev. No Python is required on the node.
|
|
96
98
|
|
|
97
99
|
Build the static daemons before release or local testing:
|
|
98
100
|
|
|
@@ -240,6 +240,10 @@ def command_node(
|
|
|
240
240
|
print(f"node: {args.name}")
|
|
241
241
|
_print_json(service.status(args.name))
|
|
242
242
|
return 0
|
|
243
|
+
if action == "bootstrap":
|
|
244
|
+
result = service.bootstrap(args.name, package_path=args.package)
|
|
245
|
+
_print_json(result)
|
|
246
|
+
return 0
|
|
243
247
|
if action in {"connect", "disconnect"}:
|
|
244
248
|
provision = None
|
|
245
249
|
if action == "connect" and args.name and not getattr(args, "all", False):
|
|
@@ -613,6 +617,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
613
617
|
)
|
|
614
618
|
node_status = node_sub.add_parser("status")
|
|
615
619
|
node_status.add_argument("name")
|
|
620
|
+
node_bootstrap = node_sub.add_parser(
|
|
621
|
+
"bootstrap",
|
|
622
|
+
help=(
|
|
623
|
+
"force-redeploy msdevd on the node "
|
|
624
|
+
"(use after local upgrade when remote version mismatches)"
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
node_bootstrap.add_argument("name")
|
|
628
|
+
node_bootstrap.add_argument(
|
|
629
|
+
"--package",
|
|
630
|
+
type=Path,
|
|
631
|
+
default=None,
|
|
632
|
+
help="optional source checkout for source-layout bootstrap",
|
|
633
|
+
)
|
|
616
634
|
node_connect = node_sub.add_parser(
|
|
617
635
|
"connect",
|
|
618
636
|
help=(
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Helpers for comparing local CLI and remote msdevd versions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .. import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def remote_daemon_version_matches(
|
|
11
|
+
ping: Any,
|
|
12
|
+
*,
|
|
13
|
+
expected: str | None = None,
|
|
14
|
+
) -> bool:
|
|
15
|
+
"""Return True when ping reports the same msdevd version as the local CLI."""
|
|
16
|
+
target = expected if expected is not None else __version__
|
|
17
|
+
if not isinstance(ping, dict):
|
|
18
|
+
return False
|
|
19
|
+
remote = ping.get("version")
|
|
20
|
+
return isinstance(remote, str) and remote == target
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_incompatible_daemon_rpc_error(exc: BaseException) -> bool:
|
|
24
|
+
"""True when the remote daemon rejected a method the current CLI requires."""
|
|
25
|
+
text = str(exc).lower()
|
|
26
|
+
return "unknown rpc method" in text
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from ..resources import Node
|
|
10
|
+
from ..daemon_version import remote_daemon_version_matches
|
|
10
11
|
from ..ssh_config import (
|
|
11
12
|
SshHostSpec,
|
|
12
13
|
list_ssh_config_hosts,
|
|
@@ -220,29 +221,52 @@ class NodeService:
|
|
|
220
221
|
name: str,
|
|
221
222
|
package_path: Path | None = None,
|
|
222
223
|
) -> dict[str, Any]:
|
|
224
|
+
"""Force-redeploy msdevd on the node (ignores current ping/version)."""
|
|
223
225
|
node = self.context.node_store.get(self._required(name, "node name"))
|
|
224
|
-
return self._ensure_agent_ready(
|
|
226
|
+
return self._ensure_agent_ready(
|
|
227
|
+
node,
|
|
228
|
+
package_path=package_path,
|
|
229
|
+
force=True,
|
|
230
|
+
)
|
|
225
231
|
|
|
226
232
|
def _ensure_agent_ready(
|
|
227
233
|
self,
|
|
228
234
|
node: Node,
|
|
229
235
|
*,
|
|
230
236
|
package_path: Path | None = None,
|
|
237
|
+
force: bool = False,
|
|
231
238
|
) -> dict[str, Any]:
|
|
232
239
|
transport = self.context.create_transport(node)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
240
|
+
reason = "forced" if force else None
|
|
241
|
+
if not force:
|
|
242
|
+
try:
|
|
243
|
+
ping = transport.call("ping")
|
|
244
|
+
if remote_daemon_version_matches(ping):
|
|
245
|
+
return {
|
|
246
|
+
"ready": True,
|
|
247
|
+
"bootstrapped": False,
|
|
248
|
+
"reason": "version_match",
|
|
249
|
+
"ping": ping if isinstance(ping, dict) else None,
|
|
250
|
+
}
|
|
251
|
+
if isinstance(ping, dict):
|
|
252
|
+
reason = "version_mismatch"
|
|
253
|
+
else:
|
|
254
|
+
reason = "unavailable"
|
|
255
|
+
except DaemonUnavailableError:
|
|
256
|
+
reason = "unavailable"
|
|
257
|
+
if not node.auto_bootstrap:
|
|
258
|
+
raise RpcError(
|
|
259
|
+
f"msdevd is not ready on node {node.name!r}; "
|
|
260
|
+
"enable auto-bootstrap on the node, or run "
|
|
261
|
+
f"`msdev node bootstrap {node.name}`"
|
|
262
|
+
) from None
|
|
263
|
+
result = self.context.bootstrap(node, package_path)
|
|
264
|
+
return {
|
|
265
|
+
"ready": True,
|
|
266
|
+
"bootstrapped": True,
|
|
267
|
+
"reason": reason or "unavailable",
|
|
268
|
+
**result,
|
|
269
|
+
}
|
|
246
270
|
|
|
247
271
|
def _connection_nodes(
|
|
248
272
|
self,
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import subprocess
|
|
6
|
-
from pathlib import Path
|
|
6
|
+
from pathlib import Path, PureWindowsPath
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from .compat import popen_kwargs
|
|
9
|
+
from .compat import IS_WINDOWS, popen_kwargs
|
|
10
|
+
|
|
11
|
+
# Characters that need quoting in OpenSSH sftp batch arguments.
|
|
12
|
+
_SFTP_SPECIAL = frozenset(' \t"\'*?[]{}\\')
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def _run_kw(env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
@@ -16,6 +19,35 @@ def _run_kw(env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
|
16
19
|
return kwargs
|
|
17
20
|
|
|
18
21
|
|
|
22
|
+
def format_sftp_local_path(path: Path) -> str:
|
|
23
|
+
"""Format a local path for an OpenSSH ``sftp -b`` argument.
|
|
24
|
+
|
|
25
|
+
Windows backslashes are treated as escapes by sftp batch mode, so convert
|
|
26
|
+
drive paths to forward slashes (``C:/Users/...``). Quote when spaces or
|
|
27
|
+
other special characters remain.
|
|
28
|
+
"""
|
|
29
|
+
resolved = path.expanduser().resolve()
|
|
30
|
+
text = str(resolved)
|
|
31
|
+
if IS_WINDOWS or "\\" in text:
|
|
32
|
+
# PureWindowsPath.as_posix keeps the drive letter and uses '/'.
|
|
33
|
+
text = PureWindowsPath(text).as_posix()
|
|
34
|
+
return _quote_sftp_arg(text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def format_sftp_remote_path(path: str) -> str:
|
|
38
|
+
"""Format a remote POSIX path for an OpenSSH ``sftp -b`` argument."""
|
|
39
|
+
return _quote_sftp_arg(path)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _quote_sftp_arg(text: str) -> str:
|
|
43
|
+
if not text:
|
|
44
|
+
return '""'
|
|
45
|
+
if any(ch in _SFTP_SPECIAL for ch in text):
|
|
46
|
+
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
|
|
47
|
+
return f'"{escaped}"'
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
|
|
19
51
|
def run_sftp_batch(
|
|
20
52
|
sftp_argv: list[str],
|
|
21
53
|
ssh_host: str,
|
|
@@ -60,8 +92,9 @@ def sftp_put_file(
|
|
|
60
92
|
timeout: float | None,
|
|
61
93
|
env: dict[str, str] | None = None,
|
|
62
94
|
) -> None:
|
|
63
|
-
local =
|
|
64
|
-
|
|
95
|
+
local = format_sftp_local_path(local_path)
|
|
96
|
+
remote = format_sftp_remote_path(remote_path)
|
|
97
|
+
batch = f"put {local} {remote}\n"
|
|
65
98
|
run_sftp_batch(
|
|
66
99
|
sftp_argv,
|
|
67
100
|
ssh_host,
|
|
@@ -82,8 +115,9 @@ def sftp_get_file(
|
|
|
82
115
|
timeout: float | None,
|
|
83
116
|
env: dict[str, str] | None = None,
|
|
84
117
|
) -> None:
|
|
85
|
-
local =
|
|
86
|
-
|
|
118
|
+
local = format_sftp_local_path(local_path)
|
|
119
|
+
remote = format_sftp_remote_path(remote_path)
|
|
120
|
+
batch = f"get {remote} {local}\n"
|
|
87
121
|
run_sftp_batch(
|
|
88
122
|
sftp_argv,
|
|
89
123
|
ssh_host,
|
|
@@ -105,10 +139,12 @@ def sftp_put_rename(
|
|
|
105
139
|
timeout: float | None,
|
|
106
140
|
env: dict[str, str] | None = None,
|
|
107
141
|
) -> None:
|
|
108
|
-
local =
|
|
142
|
+
local = format_sftp_local_path(local_path)
|
|
143
|
+
staging = format_sftp_remote_path(staging_path)
|
|
144
|
+
destination = format_sftp_remote_path(destination_path)
|
|
109
145
|
batch = (
|
|
110
|
-
f"put {local} {
|
|
111
|
-
f"rename {
|
|
146
|
+
f"put {local} {staging}\n"
|
|
147
|
+
f"rename {staging} {destination}\n"
|
|
112
148
|
)
|
|
113
149
|
run_sftp_batch(
|
|
114
150
|
sftp_argv,
|
|
@@ -19,6 +19,7 @@ import uuid
|
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
from typing import Any, Callable, Protocol
|
|
21
21
|
|
|
22
|
+
from .. import __version__
|
|
22
23
|
from .compat import (
|
|
23
24
|
kill_popen_tree,
|
|
24
25
|
popen_kwargs,
|
|
@@ -50,6 +51,10 @@ from .env_transfer import (
|
|
|
50
51
|
transfer_timeout_seconds,
|
|
51
52
|
)
|
|
52
53
|
from .daemon_deploy import select_daemon_binary
|
|
54
|
+
from .daemon_version import (
|
|
55
|
+
is_incompatible_daemon_rpc_error,
|
|
56
|
+
remote_daemon_version_matches,
|
|
57
|
+
)
|
|
53
58
|
from .local_rpc import connect_local_rpc
|
|
54
59
|
from .resources import Environment, Node
|
|
55
60
|
from .sftp_transfer import sftp_put_file
|
|
@@ -649,6 +654,53 @@ class SshRpcTransport:
|
|
|
649
654
|
self.control_persist = os.environ.get("MSDEV_SSH_CONTROL_PERSIST", "yes")
|
|
650
655
|
self._control_master = ssh_control_master_enabled()
|
|
651
656
|
self._tunnel = LocalForwardTunnel(self)
|
|
657
|
+
self._compatible_daemon = False
|
|
658
|
+
|
|
659
|
+
def _bootstrap_remote_daemon(self, reason: str) -> None:
|
|
660
|
+
print(
|
|
661
|
+
f"[msdev] node {self.node.name!r} msdevd needs update "
|
|
662
|
+
f"({reason}); bootstrapping user daemon...",
|
|
663
|
+
file=sys.stderr,
|
|
664
|
+
)
|
|
665
|
+
try:
|
|
666
|
+
bootstrap_node(self.node)
|
|
667
|
+
except Exception as bootstrap_exc:
|
|
668
|
+
raise RpcError(
|
|
669
|
+
f"automatic bootstrap failed for node {self.node.name!r}: "
|
|
670
|
+
f"{bootstrap_exc}. Retry with `msdev node bootstrap "
|
|
671
|
+
f"{self.node.name}` for detailed diagnostics."
|
|
672
|
+
) from bootstrap_exc
|
|
673
|
+
self._compatible_daemon = True
|
|
674
|
+
print(
|
|
675
|
+
f"[msdev] msdevd is ready on node {self.node.name!r}; "
|
|
676
|
+
"retrying request.",
|
|
677
|
+
file=sys.stderr,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def _ensure_compatible_daemon(self) -> None:
|
|
681
|
+
"""Redeploy when remote ping version does not match this CLI."""
|
|
682
|
+
if self._compatible_daemon:
|
|
683
|
+
return
|
|
684
|
+
try:
|
|
685
|
+
ping = self._call_once("ping")
|
|
686
|
+
except DaemonUnavailableError:
|
|
687
|
+
if not self.auto_bootstrap:
|
|
688
|
+
raise
|
|
689
|
+
self._bootstrap_remote_daemon("unavailable")
|
|
690
|
+
ping = self._call_once("ping")
|
|
691
|
+
if remote_daemon_version_matches(ping):
|
|
692
|
+
self._compatible_daemon = True
|
|
693
|
+
return
|
|
694
|
+
remote = ping.get("version") if isinstance(ping, dict) else None
|
|
695
|
+
if not self.auto_bootstrap:
|
|
696
|
+
raise RpcError(
|
|
697
|
+
f"msdevd version mismatch on node {self.node.name!r}: "
|
|
698
|
+
f"remote={remote!r} local={__version__!r}; "
|
|
699
|
+
f"run `msdev node bootstrap {self.node.name}`"
|
|
700
|
+
)
|
|
701
|
+
self._bootstrap_remote_daemon(
|
|
702
|
+
f"version mismatch remote={remote!r} local={__version__!r}"
|
|
703
|
+
)
|
|
652
704
|
|
|
653
705
|
def _rpc_target_label(self) -> str:
|
|
654
706
|
if self.environment is not None:
|
|
@@ -1143,56 +1195,43 @@ esac
|
|
|
1143
1195
|
return values
|
|
1144
1196
|
|
|
1145
1197
|
def call(self, method: str, params: dict[str, Any] | None = None) -> Any:
|
|
1198
|
+
if method != "ping":
|
|
1199
|
+
self._ensure_compatible_daemon()
|
|
1146
1200
|
try:
|
|
1147
1201
|
return self._call_once(method, params)
|
|
1148
|
-
except DaemonUnavailableError
|
|
1202
|
+
except DaemonUnavailableError:
|
|
1149
1203
|
if not self.auto_bootstrap:
|
|
1150
1204
|
raise
|
|
1151
|
-
|
|
1152
|
-
f"[msdev] node {self.node.name!r} is reachable but msdevd "
|
|
1153
|
-
"is unavailable; bootstrapping user daemon...",
|
|
1154
|
-
file=sys.stderr,
|
|
1155
|
-
)
|
|
1156
|
-
try:
|
|
1157
|
-
bootstrap_node(self.node)
|
|
1158
|
-
except Exception as bootstrap_exc:
|
|
1159
|
-
raise RpcError(
|
|
1160
|
-
f"automatic bootstrap failed for node {self.node.name!r}: "
|
|
1161
|
-
f"{bootstrap_exc}. Retry with `msdev node bootstrap "
|
|
1162
|
-
f"{self.node.name}` for detailed diagnostics."
|
|
1163
|
-
) from bootstrap_exc
|
|
1164
|
-
print(
|
|
1165
|
-
f"[msdev] msdevd is ready on node {self.node.name!r}; "
|
|
1166
|
-
"retrying request.",
|
|
1167
|
-
file=sys.stderr,
|
|
1168
|
-
)
|
|
1205
|
+
self._bootstrap_remote_daemon("unavailable")
|
|
1169
1206
|
return self._call_once(method, params)
|
|
1207
|
+
except RpcError as exc:
|
|
1208
|
+
if (
|
|
1209
|
+
method != "ping"
|
|
1210
|
+
and self.auto_bootstrap
|
|
1211
|
+
and is_incompatible_daemon_rpc_error(exc)
|
|
1212
|
+
):
|
|
1213
|
+
self._bootstrap_remote_daemon(str(exc))
|
|
1214
|
+
return self._call_once(method, params)
|
|
1215
|
+
raise
|
|
1170
1216
|
|
|
1171
1217
|
def stream_exec(
|
|
1172
1218
|
self,
|
|
1173
1219
|
params: dict[str, Any],
|
|
1174
1220
|
on_output: Callable[[str, bytes], None],
|
|
1175
1221
|
) -> dict[str, Any]:
|
|
1222
|
+
self._ensure_compatible_daemon()
|
|
1176
1223
|
try:
|
|
1177
1224
|
return self._stream_exec_once(params, on_output)
|
|
1178
|
-
except DaemonUnavailableError
|
|
1225
|
+
except DaemonUnavailableError:
|
|
1179
1226
|
if not self.auto_bootstrap:
|
|
1180
1227
|
raise
|
|
1181
|
-
|
|
1182
|
-
f"[msdev] node {self.node.name!r} is reachable but msdevd "
|
|
1183
|
-
"is unavailable; bootstrapping user daemon...",
|
|
1184
|
-
file=sys.stderr,
|
|
1185
|
-
)
|
|
1186
|
-
try:
|
|
1187
|
-
bootstrap_node(self.node)
|
|
1188
|
-
except Exception as bootstrap_exc:
|
|
1189
|
-
raise RpcError(
|
|
1190
|
-
f"automatic bootstrap failed for node "
|
|
1191
|
-
f"{self.node.name!r}: {bootstrap_exc}. Retry with "
|
|
1192
|
-
f"`msdev node connect {self.node.name}` for "
|
|
1193
|
-
"detailed diagnostics."
|
|
1194
|
-
) from bootstrap_exc
|
|
1228
|
+
self._bootstrap_remote_daemon("unavailable")
|
|
1195
1229
|
return self._stream_exec_once(params, on_output)
|
|
1230
|
+
except RpcError as exc:
|
|
1231
|
+
if self.auto_bootstrap and is_incompatible_daemon_rpc_error(exc):
|
|
1232
|
+
self._bootstrap_remote_daemon(str(exc))
|
|
1233
|
+
return self._stream_exec_once(params, on_output)
|
|
1234
|
+
raise
|
|
1196
1235
|
|
|
1197
1236
|
def _stream_exec_once(
|
|
1198
1237
|
self,
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
e397d2c3621ad047df7bc181b2fa7637694fb533fa559a39e3495130d3fbf5fa msdevd-linux-amd64
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
86aa45ed2e836e359cc65fe341c39aa84c61bde4f522f086fdecac7026695627 msdevd-linux-arm64
|
|
@@ -56,6 +56,7 @@ msdev node sync-ssh
|
|
|
56
56
|
msdev node list --ssh
|
|
57
57
|
msdev node connect NODE
|
|
58
58
|
msdev node connect NODE --identity FILE
|
|
59
|
+
msdev node bootstrap NODE
|
|
59
60
|
msdev node status NODE
|
|
60
61
|
msdev node disconnect NODE
|
|
61
62
|
msdev node remove NODE
|
|
@@ -96,7 +97,9 @@ registers existing Host blocks.
|
|
|
96
97
|
|
|
97
98
|
`msdev node connect` opens SSH persistence, saves `--password` into SSH config,
|
|
98
99
|
installs the local public key when a password is available, and deploys/starts
|
|
99
|
-
`msdevd` when
|
|
100
|
+
`msdevd` when missing or when the remote `ping` version does not match the
|
|
101
|
+
local CLI. Use `msdev node bootstrap NODE` to force a redeploy after upgrading
|
|
102
|
+
msdev (version mismatch is not always visible as a hard failure).
|
|
100
103
|
|
|
101
104
|
`msdev env open` runs the env `setup` script once and establishes the shared
|
|
102
105
|
session. `msdev exec` opens the env automatically when needed.
|
|
@@ -178,5 +181,8 @@ captures one result. Local cancellation terminates the remote process group.
|
|
|
178
181
|
|
|
179
182
|
- Ask before destructive or externally visible changes unless authorized.
|
|
180
183
|
- Track temporary resources and remove only those created by the task.
|
|
181
|
-
- If `msdevd` is missing
|
|
184
|
+
- If `msdevd` is missing, version-mismatched, or returns
|
|
185
|
+
`unknown RPC method` (for example `env.open`), run
|
|
186
|
+
`msdev node bootstrap NODE` (or retry the command — exec/connect now
|
|
187
|
+
auto-redeploy when `auto_bootstrap` is on), then retry.
|
|
182
188
|
- If connect fails, report the error; do not bypass msdev with another transport.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: msdev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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
|
|
@@ -104,6 +104,7 @@ key, and deploys `msdevd`:
|
|
|
104
104
|
```bash
|
|
105
105
|
msdev node connect dev-122 --hostname 80.48.33.133 --user root --password PASSWORD
|
|
106
106
|
msdev node connect dev-122
|
|
107
|
+
msdev node bootstrap dev-122
|
|
107
108
|
msdev node sync-ssh
|
|
108
109
|
msdev node list --ssh
|
|
109
110
|
msdev node list
|
|
@@ -116,8 +117,9 @@ config. `node sync-ssh` registers Host aliases that were added manually.
|
|
|
116
117
|
|
|
117
118
|
Connect atomically creates a same-named host Env on first use and uploads a
|
|
118
119
|
**static Go msdevd** binary matching the node architecture
|
|
119
|
-
(`linux-amd64` / `linux-arm64`) when the agent is missing or
|
|
120
|
-
|
|
120
|
+
(`linux-amd64` / `linux-arm64`) when the agent is missing or when the remote
|
|
121
|
+
`ping` version does not match the local CLI. Use **`msdev node bootstrap NODE`**
|
|
122
|
+
to force a redeploy after upgrading msdev. No Python is required on the node.
|
|
121
123
|
|
|
122
124
|
Build the static daemons before release or local testing:
|
|
123
125
|
|
|
@@ -15,6 +15,7 @@ src/msdev/core/config.py
|
|
|
15
15
|
src/msdev/core/config_bundle.py
|
|
16
16
|
src/msdev/core/container_helper.py
|
|
17
17
|
src/msdev/core/daemon_deploy.py
|
|
18
|
+
src/msdev/core/daemon_version.py
|
|
18
19
|
src/msdev/core/env_transfer.py
|
|
19
20
|
src/msdev/core/exec_argv.py
|
|
20
21
|
src/msdev/core/guides.py
|
|
@@ -81,6 +82,7 @@ tests/test_model_cli.py
|
|
|
81
82
|
tests/test_model_services.py
|
|
82
83
|
tests/test_msdev_cli_skill.py
|
|
83
84
|
tests/test_multiplex.py
|
|
85
|
+
tests/test_node_bootstrap_version_scenario.py
|
|
84
86
|
tests/test_node_connect_provision_scenario.py
|
|
85
87
|
tests/test_node_connect_save_password_scenario.py
|
|
86
88
|
tests/test_node_env_config.py
|
|
@@ -91,6 +93,7 @@ tests/test_sftp_transfer_scenario.py
|
|
|
91
93
|
tests/test_ssh_auth.py
|
|
92
94
|
tests/test_ssh_config.py
|
|
93
95
|
tests/test_ssh_tunnel.py
|
|
96
|
+
tests/test_stale_daemon_autobootstrap_scenario.py
|
|
94
97
|
tests/test_stream_server.py
|
|
95
98
|
tests/test_stream_transport.py
|
|
96
99
|
tests/test_streamproto.py
|
|
@@ -307,6 +307,27 @@ class CliParserTest(unittest.TestCase):
|
|
|
307
307
|
{"node_id": "node-1"},
|
|
308
308
|
'node: worker\n{\n "node_id": "node-1"\n}\n',
|
|
309
309
|
),
|
|
310
|
+
(
|
|
311
|
+
["node", "bootstrap", "worker"],
|
|
312
|
+
"bootstrap",
|
|
313
|
+
{
|
|
314
|
+
"ready": True,
|
|
315
|
+
"bootstrapped": True,
|
|
316
|
+
"reason": "forced",
|
|
317
|
+
"daemon": {"ok": True, "version": "0.14.1"},
|
|
318
|
+
},
|
|
319
|
+
(
|
|
320
|
+
"{\n"
|
|
321
|
+
' "bootstrapped": true,\n'
|
|
322
|
+
' "daemon": {\n'
|
|
323
|
+
' "ok": true,\n'
|
|
324
|
+
' "version": "0.14.1"\n'
|
|
325
|
+
" },\n"
|
|
326
|
+
' "ready": true,\n'
|
|
327
|
+
' "reason": "forced"\n'
|
|
328
|
+
"}\n"
|
|
329
|
+
),
|
|
330
|
+
),
|
|
310
331
|
(
|
|
311
332
|
["node", "connect", "worker"],
|
|
312
333
|
"connect",
|
|
@@ -30,8 +30,13 @@ class CliOnlyArchitectureTests(unittest.TestCase):
|
|
|
30
30
|
parser.parse_args(["npu", "list", "--node", "worker"])
|
|
31
31
|
with self.assertRaises(SystemExit):
|
|
32
32
|
parser.parse_args(["model", "list", "--node", "worker"])
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
|
|
34
|
+
def test_node_bootstrap_is_present(self):
|
|
35
|
+
args = build_parser().parse_args(["node", "bootstrap", "worker"])
|
|
36
|
+
self.assertEqual(args.command, "node")
|
|
37
|
+
self.assertEqual(args.node_command, "bootstrap")
|
|
38
|
+
self.assertEqual(args.name, "worker")
|
|
39
|
+
self.assertIsNone(args.package)
|
|
35
40
|
|
|
36
41
|
def test_skill_command_is_absent(self):
|
|
37
42
|
parser = build_parser()
|
|
@@ -56,7 +56,7 @@ class ConfigBundleTests(unittest.TestCase):
|
|
|
56
56
|
summary = export_bundle(
|
|
57
57
|
export_dir,
|
|
58
58
|
config_path=resources,
|
|
59
|
-
msdev_version="0.
|
|
59
|
+
msdev_version="0.14.1",
|
|
60
60
|
)
|
|
61
61
|
self.assertFalse(summary["archive"])
|
|
62
62
|
self.assertTrue((export_dir / BUNDLE_MANIFEST).is_file())
|
|
@@ -100,7 +100,7 @@ class ConfigBundleTests(unittest.TestCase):
|
|
|
100
100
|
)
|
|
101
101
|
|
|
102
102
|
archive = root / "msdev-config.tar.gz"
|
|
103
|
-
export_bundle(archive, config_path=resources, msdev_version="0.
|
|
103
|
+
export_bundle(archive, config_path=resources, msdev_version="0.14.1")
|
|
104
104
|
self.assertTrue(archive.is_file())
|
|
105
105
|
|
|
106
106
|
target_resources = root / "imported" / "resources.json"
|
|
@@ -126,7 +126,7 @@ class ConfigBundleTests(unittest.TestCase):
|
|
|
126
126
|
)
|
|
127
127
|
|
|
128
128
|
bundle_dir = root / "bundle"
|
|
129
|
-
export_bundle(bundle_dir, config_path=source_resources, msdev_version="0.
|
|
129
|
+
export_bundle(bundle_dir, config_path=source_resources, msdev_version="0.14.1")
|
|
130
130
|
result = import_bundle(bundle_dir, config_path=target_resources)
|
|
131
131
|
self.assertEqual(result["imported_nodes"], 0)
|
|
132
132
|
self.assertIn("node:worker", result["skipped"])
|
|
@@ -683,7 +683,7 @@ class RpcDispatcherTest(unittest.TestCase):
|
|
|
683
683
|
started = time.monotonic()
|
|
684
684
|
first = json.loads(proxy.stdout.readline())
|
|
685
685
|
self.assertEqual(first["event"], "stdout")
|
|
686
|
-
self.assertLess(time.monotonic() - started, 0
|
|
686
|
+
self.assertLess(time.monotonic() - started, 1.0)
|
|
687
687
|
events = [first]
|
|
688
688
|
while events[-1]["event"] != "result":
|
|
689
689
|
events.append(json.loads(proxy.stdout.readline()))
|
|
@@ -104,7 +104,7 @@ class GoMsdevdScenarioTests(unittest.TestCase):
|
|
|
104
104
|
self.assertNotIn("error", response)
|
|
105
105
|
result = response["result"]
|
|
106
106
|
self.assertTrue(result["ok"])
|
|
107
|
-
self.assertEqual(result["version"], "0.
|
|
107
|
+
self.assertEqual(result["version"], "0.14.1")
|
|
108
108
|
self.assertIn("node_id", result)
|
|
109
109
|
|
|
110
110
|
def test_exec(self):
|
|
@@ -45,9 +45,9 @@ class MsdevCliSkillTests(unittest.TestCase):
|
|
|
45
45
|
self.assertIn("msdev env open", content)
|
|
46
46
|
self.assertIn("msdev exec ENV", content)
|
|
47
47
|
self.assertIn("msdev node connect", content)
|
|
48
|
+
self.assertIn("msdev node bootstrap", content)
|
|
48
49
|
self.assertIn("msdev node guide NODE", content)
|
|
49
50
|
self.assertIn("msdev env shell ENV", content)
|
|
50
|
-
self.assertNotIn("msdev node bootstrap", content)
|
|
51
51
|
self.assertNotIn("msdev model list", content)
|
|
52
52
|
self.assertNotIn("msdev npu list", content)
|
|
53
53
|
self.assertIn("Do not use MCP", content)
|
|
@@ -61,6 +61,7 @@ class MsdevCliSkillTests(unittest.TestCase):
|
|
|
61
61
|
required_commands = (
|
|
62
62
|
"msdev node connect",
|
|
63
63
|
"msdev node connect NODE --hostname",
|
|
64
|
+
"msdev node bootstrap NODE",
|
|
64
65
|
"msdev node remove",
|
|
65
66
|
"msdev env add",
|
|
66
67
|
"msdev env open",
|