msdev 0.13.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {msdev-0.13.0 → msdev-0.14.0}/PKG-INFO +5 -3
- {msdev-0.13.0 → msdev-0.14.0}/README.md +4 -2
- {msdev-0.13.0 → msdev-0.14.0}/pyproject.toml +1 -1
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/__init__.py +1 -1
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/cli.py +18 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/node.py +51 -14
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/sftp_transfer.py +45 -9
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdevd-linux-amd64 +0 -0
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +1 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdevd-linux-arm64 +0 -0
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +1 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/skills/msdev-cli/SKILL.md +6 -2
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev.egg-info/PKG-INFO +5 -3
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev.egg-info/SOURCES.txt +1 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_cli.py +21 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_cli_only_architecture.py +7 -2
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_config_bundle.py +3 -3
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_daemon.py +1 -1
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_go_msdevd_scenario.py +1 -1
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_msdev_cli_skill.py +2 -1
- msdev-0.14.0/tests/test_node_bootstrap_version_scenario.py +173 -0
- msdev-0.14.0/tests/test_sftp_transfer_scenario.py +239 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_target_exec_services.py +68 -9
- 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.0}/LICENSE +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/setup.cfg +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/__init__.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/compat.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/container_helper.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/daemon_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/guides.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/helper_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/host_files.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/host_mutations.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/inventory.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/limits.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/local_rpc.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/multiplex.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/request_client.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/resources.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/context.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/environment.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/model.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/services/results.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/ssh_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/ssh_tunnel.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/stream_limits.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/stream_server.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/stream_transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/streamproto.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/core/transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/daemon.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev.egg-info/entry_points.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_compat.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_connect_env_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_container_helper.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_daemon_container_stream.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_daemon_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_docker_stream_dispatch.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_env_transfer.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_exec_argv.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_exec_command_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_exec_default_timeout_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_helper_deploy.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_init_skill.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_inventory.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_model_cli.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_model_services.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_multiplex.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_node_connect_provision_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_node_connect_save_password_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_node_env_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_node_env_services.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_request_client.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_ssh_auth.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_ssh_config.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_ssh_tunnel.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_stream_server.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_stream_transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_streamproto.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_target_guides.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/tests/test_transport.py +0 -0
- {msdev-0.13.0 → msdev-0.14.0}/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.0
|
|
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=(
|
|
@@ -6,6 +6,7 @@ from dataclasses import asdict, dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from ... import __version__
|
|
9
10
|
from ..resources import Node
|
|
10
11
|
from ..ssh_config import (
|
|
11
12
|
SshHostSpec,
|
|
@@ -18,6 +19,19 @@ from ..transport import DaemonUnavailableError, RpcError
|
|
|
18
19
|
from .context import ServiceContext
|
|
19
20
|
|
|
20
21
|
|
|
22
|
+
def remote_daemon_version_matches(
|
|
23
|
+
ping: Any,
|
|
24
|
+
*,
|
|
25
|
+
expected: str | None = None,
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""Return True when ping reports the same msdevd version as the local CLI."""
|
|
28
|
+
target = expected if expected is not None else __version__
|
|
29
|
+
if not isinstance(ping, dict):
|
|
30
|
+
return False
|
|
31
|
+
remote = ping.get("version")
|
|
32
|
+
return isinstance(remote, str) and remote == target
|
|
33
|
+
|
|
34
|
+
|
|
21
35
|
@dataclass(frozen=True)
|
|
22
36
|
class NodeAddRequest:
|
|
23
37
|
name: str
|
|
@@ -220,29 +234,52 @@ class NodeService:
|
|
|
220
234
|
name: str,
|
|
221
235
|
package_path: Path | None = None,
|
|
222
236
|
) -> dict[str, Any]:
|
|
237
|
+
"""Force-redeploy msdevd on the node (ignores current ping/version)."""
|
|
223
238
|
node = self.context.node_store.get(self._required(name, "node name"))
|
|
224
|
-
return self._ensure_agent_ready(
|
|
239
|
+
return self._ensure_agent_ready(
|
|
240
|
+
node,
|
|
241
|
+
package_path=package_path,
|
|
242
|
+
force=True,
|
|
243
|
+
)
|
|
225
244
|
|
|
226
245
|
def _ensure_agent_ready(
|
|
227
246
|
self,
|
|
228
247
|
node: Node,
|
|
229
248
|
*,
|
|
230
249
|
package_path: Path | None = None,
|
|
250
|
+
force: bool = False,
|
|
231
251
|
) -> dict[str, Any]:
|
|
232
252
|
transport = self.context.create_transport(node)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
reason = "forced" if force else None
|
|
254
|
+
if not force:
|
|
255
|
+
try:
|
|
256
|
+
ping = transport.call("ping")
|
|
257
|
+
if remote_daemon_version_matches(ping):
|
|
258
|
+
return {
|
|
259
|
+
"ready": True,
|
|
260
|
+
"bootstrapped": False,
|
|
261
|
+
"reason": "version_match",
|
|
262
|
+
"ping": ping if isinstance(ping, dict) else None,
|
|
263
|
+
}
|
|
264
|
+
if isinstance(ping, dict):
|
|
265
|
+
reason = "version_mismatch"
|
|
266
|
+
else:
|
|
267
|
+
reason = "unavailable"
|
|
268
|
+
except DaemonUnavailableError:
|
|
269
|
+
reason = "unavailable"
|
|
270
|
+
if not node.auto_bootstrap:
|
|
271
|
+
raise RpcError(
|
|
272
|
+
f"msdevd is not ready on node {node.name!r}; "
|
|
273
|
+
"enable auto-bootstrap on the node, or run "
|
|
274
|
+
f"`msdev node bootstrap {node.name}`"
|
|
275
|
+
) from None
|
|
276
|
+
result = self.context.bootstrap(node, package_path)
|
|
277
|
+
return {
|
|
278
|
+
"ready": True,
|
|
279
|
+
"bootstrapped": True,
|
|
280
|
+
"reason": reason or "unavailable",
|
|
281
|
+
**result,
|
|
282
|
+
}
|
|
246
283
|
|
|
247
284
|
def _connection_nodes(
|
|
248
285
|
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,
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
7b3d2dcd31cc752b44289c5b0936569be7df1aef162d07c1b3c00e886cfea884 msdevd-linux-amd64
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aeb5721e49746058e286ed14621208823ee092372c0a7d772cfdd9924d5b71e1 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,6 @@ 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 or
|
|
184
|
+
- If `msdevd` is missing or its version mismatches the local CLI, run
|
|
185
|
+
`msdev node bootstrap NODE` (or `msdev node connect NODE`), then retry.
|
|
182
186
|
- 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.0
|
|
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
|
|
|
@@ -81,6 +81,7 @@ tests/test_model_cli.py
|
|
|
81
81
|
tests/test_model_services.py
|
|
82
82
|
tests/test_msdev_cli_skill.py
|
|
83
83
|
tests/test_multiplex.py
|
|
84
|
+
tests/test_node_bootstrap_version_scenario.py
|
|
84
85
|
tests/test_node_connect_provision_scenario.py
|
|
85
86
|
tests/test_node_connect_save_password_scenario.py
|
|
86
87
|
tests/test_node_env_config.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.0"},
|
|
318
|
+
},
|
|
319
|
+
(
|
|
320
|
+
"{\n"
|
|
321
|
+
' "bootstrapped": true,\n'
|
|
322
|
+
' "daemon": {\n'
|
|
323
|
+
' "ok": true,\n'
|
|
324
|
+
' "version": "0.14.0"\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.0",
|
|
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.0")
|
|
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.0")
|
|
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.0")
|
|
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",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scenario: CLI node bootstrap force-redeploys; connect matches ping version."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import Mock, patch
|
|
12
|
+
|
|
13
|
+
from msdev import __version__
|
|
14
|
+
from msdev.cli import build_parser, command_node
|
|
15
|
+
from msdev.core.resources import EnvironmentStore, Node, NodeStore
|
|
16
|
+
from msdev.core.services.context import ServiceContext
|
|
17
|
+
from msdev.core.services.node import NodeService, remote_daemon_version_matches
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NodeBootstrapVersionScenarioTests(unittest.TestCase):
|
|
21
|
+
def test_remote_daemon_version_matches_helper(self) -> None:
|
|
22
|
+
self.assertTrue(
|
|
23
|
+
remote_daemon_version_matches(
|
|
24
|
+
{"ok": True, "version": __version__},
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
self.assertFalse(
|
|
28
|
+
remote_daemon_version_matches(
|
|
29
|
+
{"ok": True, "version": "0.0.0"},
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
self.assertFalse(remote_daemon_version_matches({"ok": True}))
|
|
33
|
+
self.assertFalse(remote_daemon_version_matches("not-a-dict"))
|
|
34
|
+
|
|
35
|
+
def test_cli_bootstrap_force_redeploys_even_when_ping_succeeds(self) -> None:
|
|
36
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
37
|
+
root = Path(temp_dir)
|
|
38
|
+
resources = root / "resources.json"
|
|
39
|
+
node_store = NodeStore(resources)
|
|
40
|
+
node_store.add(
|
|
41
|
+
Node(name="worker", ssh_host="worker", auto_bootstrap=True)
|
|
42
|
+
)
|
|
43
|
+
environment_store = EnvironmentStore(resources)
|
|
44
|
+
|
|
45
|
+
bootstrap_result = {
|
|
46
|
+
"ready": True,
|
|
47
|
+
"bootstrapped": True,
|
|
48
|
+
"reason": "forced",
|
|
49
|
+
"daemon": {
|
|
50
|
+
"ok": True,
|
|
51
|
+
"version": __version__,
|
|
52
|
+
"node_id": "n1",
|
|
53
|
+
},
|
|
54
|
+
"layout": "static",
|
|
55
|
+
}
|
|
56
|
+
args = build_parser().parse_args(
|
|
57
|
+
["--config", str(resources), "node", "bootstrap", "worker"]
|
|
58
|
+
)
|
|
59
|
+
stdout = StringIO()
|
|
60
|
+
with (
|
|
61
|
+
patch("msdev.cli.NodeService") as service_cls,
|
|
62
|
+
patch("sys.stdout", stdout),
|
|
63
|
+
):
|
|
64
|
+
service = service_cls.return_value
|
|
65
|
+
service.bootstrap.return_value = bootstrap_result
|
|
66
|
+
code = command_node(args, node_store, environment_store)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(code, 0)
|
|
69
|
+
service.bootstrap.assert_called_once_with("worker", package_path=None)
|
|
70
|
+
payload = json.loads(stdout.getvalue())
|
|
71
|
+
self.assertTrue(payload["bootstrapped"])
|
|
72
|
+
self.assertEqual(payload["reason"], "forced")
|
|
73
|
+
self.assertEqual(payload["daemon"]["version"], __version__)
|
|
74
|
+
|
|
75
|
+
def test_connect_redeploys_when_remote_version_mismatches(self) -> None:
|
|
76
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
77
|
+
root = Path(temp_dir)
|
|
78
|
+
resources = root / "resources.json"
|
|
79
|
+
node = Node(name="worker", ssh_host="worker", auto_bootstrap=True)
|
|
80
|
+
node_store = NodeStore(resources)
|
|
81
|
+
node_store.add(node)
|
|
82
|
+
environment_store = EnvironmentStore(resources)
|
|
83
|
+
|
|
84
|
+
transport = Mock()
|
|
85
|
+
transport.connect.return_value = {
|
|
86
|
+
"connected": True,
|
|
87
|
+
"already_connected": False,
|
|
88
|
+
"persist": "yes",
|
|
89
|
+
}
|
|
90
|
+
transport.call.return_value = {
|
|
91
|
+
"ok": True,
|
|
92
|
+
"version": "0.12.0",
|
|
93
|
+
"node_id": "old",
|
|
94
|
+
}
|
|
95
|
+
bootstrap = Mock(
|
|
96
|
+
return_value={
|
|
97
|
+
"daemon": {
|
|
98
|
+
"ok": True,
|
|
99
|
+
"version": __version__,
|
|
100
|
+
"node_id": "new",
|
|
101
|
+
},
|
|
102
|
+
"layout": "static",
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
context = ServiceContext(
|
|
106
|
+
node_store=node_store,
|
|
107
|
+
environment_store=environment_store,
|
|
108
|
+
create_transport=Mock(return_value=transport),
|
|
109
|
+
bootstrap=bootstrap,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
args = build_parser().parse_args(
|
|
113
|
+
["--config", str(resources), "node", "connect", "worker"]
|
|
114
|
+
)
|
|
115
|
+
stdout = StringIO()
|
|
116
|
+
with (
|
|
117
|
+
patch("msdev.cli._service_context", return_value=context),
|
|
118
|
+
patch("msdev.cli.NodeService", NodeService),
|
|
119
|
+
patch("sys.stdout", stdout),
|
|
120
|
+
):
|
|
121
|
+
code = command_node(args, node_store, environment_store)
|
|
122
|
+
|
|
123
|
+
self.assertEqual(code, 0, stdout.getvalue())
|
|
124
|
+
bootstrap.assert_called_once_with(node, None)
|
|
125
|
+
self.assertIn("agent=ready", stdout.getvalue())
|
|
126
|
+
transport.call.assert_called_with("ping")
|
|
127
|
+
|
|
128
|
+
def test_connect_skips_redeploy_when_version_matches(self) -> None:
|
|
129
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
130
|
+
root = Path(temp_dir)
|
|
131
|
+
resources = root / "resources.json"
|
|
132
|
+
node = Node(name="worker", ssh_host="worker", auto_bootstrap=True)
|
|
133
|
+
node_store = NodeStore(resources)
|
|
134
|
+
node_store.add(node)
|
|
135
|
+
environment_store = EnvironmentStore(resources)
|
|
136
|
+
|
|
137
|
+
transport = Mock()
|
|
138
|
+
transport.connect.return_value = {
|
|
139
|
+
"connected": True,
|
|
140
|
+
"already_connected": False,
|
|
141
|
+
"persist": "yes",
|
|
142
|
+
}
|
|
143
|
+
transport.call.return_value = {
|
|
144
|
+
"ok": True,
|
|
145
|
+
"version": __version__,
|
|
146
|
+
"node_id": "n1",
|
|
147
|
+
}
|
|
148
|
+
bootstrap = Mock()
|
|
149
|
+
context = ServiceContext(
|
|
150
|
+
node_store=node_store,
|
|
151
|
+
environment_store=environment_store,
|
|
152
|
+
create_transport=Mock(return_value=transport),
|
|
153
|
+
bootstrap=bootstrap,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
args = build_parser().parse_args(
|
|
157
|
+
["--config", str(resources), "node", "connect", "worker"]
|
|
158
|
+
)
|
|
159
|
+
stdout = StringIO()
|
|
160
|
+
with (
|
|
161
|
+
patch("msdev.cli._service_context", return_value=context),
|
|
162
|
+
patch("msdev.cli.NodeService", NodeService),
|
|
163
|
+
patch("sys.stdout", stdout),
|
|
164
|
+
):
|
|
165
|
+
code = command_node(args, node_store, environment_store)
|
|
166
|
+
|
|
167
|
+
self.assertEqual(code, 0, stdout.getvalue())
|
|
168
|
+
bootstrap.assert_not_called()
|
|
169
|
+
self.assertIn("agent=ready", stdout.getvalue())
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
unittest.main()
|