msdev 0.14.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.14.0 → msdev-0.14.1}/PKG-INFO +1 -1
- {msdev-0.14.0 → msdev-0.14.1}/pyproject.toml +1 -1
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/__init__.py +1 -1
- msdev-0.14.1/src/msdev/core/daemon_version.py +26 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/node.py +1 -14
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/transport.py +73 -34
- {msdev-0.14.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.14.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.14.0 → msdev-0.14.1}/src/msdev/skills/msdev-cli/SKILL.md +4 -2
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/PKG-INFO +1 -1
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/SOURCES.txt +2 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_cli.py +2 -2
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_config_bundle.py +3 -3
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_go_msdevd_scenario.py +1 -1
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_bootstrap_version_scenario.py +2 -1
- msdev-0.14.1/tests/test_stale_daemon_autobootstrap_scenario.py +100 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_transport.py +4 -0
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-amd64.sha256 +0 -1
- msdev-0.14.0/src/msdev/helpers/msdevd-linux-arm64.sha256 +0 -1
- {msdev-0.14.0 → msdev-0.14.1}/LICENSE +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/README.md +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/setup.cfg +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/cli.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/__init__.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/compat.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/config.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/config_bundle.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/container_helper.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/daemon_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/env_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/exec_argv.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/guides.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/helper_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/host_files.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/host_mutations.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/init_setup.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/inventory.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/limits.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/local_rpc.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/multiplex.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/request_client.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/resources.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/__init__.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/capabilities.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/context.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/environment.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/execution.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/model.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/npu.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/services/results.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/sftp_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/skill_install.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_auth.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_config.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/ssh_tunnel.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_limits.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_server.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/stream_transport.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/core/streamproto.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/daemon.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64 +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-amd64.sha256 +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64 +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev/helpers/msdev-file-helper-linux-arm64.sha256 +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/dependency_links.txt +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/entry_points.txt +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/src/msdev.egg-info/top_level.txt +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_cli_only_architecture.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_compat.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_config.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_connect_env_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_container_helper.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon_container_stream.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_daemon_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_docker_stream_dispatch.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_put_get_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_read_write_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_env_transfer.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_argv.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_command_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_exec_default_timeout_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_helper_deploy.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_init_skill.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_inventory.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_model_cli.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_model_services.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_msdev_cli_skill.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_multiplex.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_connect_provision_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_connect_save_password_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_env_config.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_env_services.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_node_sync_ssh_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_request_client.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_sftp_transfer_scenario.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_auth.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_config.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_ssh_tunnel.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_stream_server.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_stream_transport.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_streamproto.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_target_exec_services.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_target_guides.py +0 -0
- {msdev-0.14.0 → msdev-0.14.1}/tests/test_windows_local_scenario.py +0 -0
|
@@ -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
|
|
@@ -6,8 +6,8 @@ from dataclasses import asdict, dataclass
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
-
from ... import __version__
|
|
10
9
|
from ..resources import Node
|
|
10
|
+
from ..daemon_version import remote_daemon_version_matches
|
|
11
11
|
from ..ssh_config import (
|
|
12
12
|
SshHostSpec,
|
|
13
13
|
list_ssh_config_hosts,
|
|
@@ -19,19 +19,6 @@ from ..transport import DaemonUnavailableError, RpcError
|
|
|
19
19
|
from .context import ServiceContext
|
|
20
20
|
|
|
21
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
|
-
|
|
35
22
|
@dataclass(frozen=True)
|
|
36
23
|
class NodeAddRequest:
|
|
37
24
|
name: str
|
|
@@ -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
|
|
@@ -181,6 +181,8 @@ captures one result. Local cancellation terminates the remote process group.
|
|
|
181
181
|
|
|
182
182
|
- Ask before destructive or externally visible changes unless authorized.
|
|
183
183
|
- Track temporary resources and remove only those created by the task.
|
|
184
|
-
- If `msdevd` is missing
|
|
185
|
-
`
|
|
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.
|
|
186
188
|
- If connect fails, report the error; do not bypass msdev with another transport.
|
|
@@ -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
|
|
@@ -92,6 +93,7 @@ tests/test_sftp_transfer_scenario.py
|
|
|
92
93
|
tests/test_ssh_auth.py
|
|
93
94
|
tests/test_ssh_config.py
|
|
94
95
|
tests/test_ssh_tunnel.py
|
|
96
|
+
tests/test_stale_daemon_autobootstrap_scenario.py
|
|
95
97
|
tests/test_stream_server.py
|
|
96
98
|
tests/test_stream_transport.py
|
|
97
99
|
tests/test_streamproto.py
|
|
@@ -314,14 +314,14 @@ class CliParserTest(unittest.TestCase):
|
|
|
314
314
|
"ready": True,
|
|
315
315
|
"bootstrapped": True,
|
|
316
316
|
"reason": "forced",
|
|
317
|
-
"daemon": {"ok": True, "version": "0.14.
|
|
317
|
+
"daemon": {"ok": True, "version": "0.14.1"},
|
|
318
318
|
},
|
|
319
319
|
(
|
|
320
320
|
"{\n"
|
|
321
321
|
' "bootstrapped": true,\n'
|
|
322
322
|
' "daemon": {\n'
|
|
323
323
|
' "ok": true,\n'
|
|
324
|
-
' "version": "0.14.
|
|
324
|
+
' "version": "0.14.1"\n'
|
|
325
325
|
" },\n"
|
|
326
326
|
' "ready": true,\n'
|
|
327
327
|
' "reason": "forced"\n'
|
|
@@ -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.14.
|
|
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.14.
|
|
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.14.
|
|
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"])
|
|
@@ -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.14.
|
|
107
|
+
self.assertEqual(result["version"], "0.14.1")
|
|
108
108
|
self.assertIn("node_id", result)
|
|
109
109
|
|
|
110
110
|
def test_exec(self):
|
|
@@ -11,10 +11,11 @@ from pathlib import Path
|
|
|
11
11
|
from unittest.mock import Mock, patch
|
|
12
12
|
|
|
13
13
|
from msdev import __version__
|
|
14
|
+
from msdev.core.daemon_version import remote_daemon_version_matches
|
|
14
15
|
from msdev.cli import build_parser, command_node
|
|
15
16
|
from msdev.core.resources import EnvironmentStore, Node, NodeStore
|
|
16
17
|
from msdev.core.services.context import ServiceContext
|
|
17
|
-
from msdev.core.services.node import NodeService
|
|
18
|
+
from msdev.core.services.node import NodeService
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class NodeBootstrapVersionScenarioTests(unittest.TestCase):
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Scenario: exec against a stale remote msdevd auto-bootstraps.
|
|
3
|
+
|
|
4
|
+
Covers the failure mode where ping still works but required RPCs such as
|
|
5
|
+
``env.open`` return ``unknown RPC method``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import unittest
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
13
|
+
from msdev import __version__
|
|
14
|
+
from msdev.core.resources import Environment, Node
|
|
15
|
+
from msdev.core.transport import RpcError, SshRpcTransport
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StaleDaemonAutoBootstrapScenarioTests(unittest.TestCase):
|
|
19
|
+
def test_env_open_unknown_method_triggers_bootstrap_then_retry(self) -> None:
|
|
20
|
+
node = Node(name="worker", ssh_host="worker.example")
|
|
21
|
+
env = Environment(name="worker", node="worker")
|
|
22
|
+
transport = SshRpcTransport(node, env, auto_bootstrap=True)
|
|
23
|
+
calls: list[str] = []
|
|
24
|
+
|
|
25
|
+
def fake_once(method, params=None):
|
|
26
|
+
calls.append(method)
|
|
27
|
+
if method == "ping":
|
|
28
|
+
# Version already matches — old binary still lacks env.open.
|
|
29
|
+
return {"ok": True, "version": __version__, "node_id": "n1"}
|
|
30
|
+
if method == "env.open":
|
|
31
|
+
if calls.count("env.open") == 1:
|
|
32
|
+
raise RpcError("KeyError: unknown RPC method: env.open")
|
|
33
|
+
return {"open": True, "ready": True, "name": "worker"}
|
|
34
|
+
raise AssertionError(f"unexpected method {method}")
|
|
35
|
+
|
|
36
|
+
with patch(
|
|
37
|
+
"msdev.core.transport.bootstrap_node",
|
|
38
|
+
return_value={"layout": "static"},
|
|
39
|
+
) as bootstrap:
|
|
40
|
+
with patch.object(transport, "_call_once", side_effect=fake_once):
|
|
41
|
+
with patch.object(transport, "_ensure_mux"):
|
|
42
|
+
result = transport.call(
|
|
43
|
+
"env.open",
|
|
44
|
+
{"_env": env.to_agent_spec()},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.assertEqual(result["ready"], True)
|
|
48
|
+
bootstrap.assert_called_once_with(node)
|
|
49
|
+
self.assertGreaterEqual(calls.count("env.open"), 2)
|
|
50
|
+
|
|
51
|
+
def test_exec_path_bootstraps_on_version_mismatch_before_env_open(self) -> None:
|
|
52
|
+
node = Node(name="worker", ssh_host="worker.example")
|
|
53
|
+
env = Environment(name="worker", node="worker")
|
|
54
|
+
transport = SshRpcTransport(node, env, auto_bootstrap=True)
|
|
55
|
+
calls: list[str] = []
|
|
56
|
+
|
|
57
|
+
def fake_once(method, params=None):
|
|
58
|
+
calls.append(method)
|
|
59
|
+
if method == "ping":
|
|
60
|
+
return {"ok": True, "version": "0.12.0", "node_id": "old"}
|
|
61
|
+
if method == "env.open":
|
|
62
|
+
return {"open": True, "ready": True, "name": "worker"}
|
|
63
|
+
raise AssertionError(f"unexpected method {method}")
|
|
64
|
+
|
|
65
|
+
with patch(
|
|
66
|
+
"msdev.core.transport.bootstrap_node",
|
|
67
|
+
return_value={"layout": "static"},
|
|
68
|
+
) as bootstrap:
|
|
69
|
+
with patch.object(transport, "_call_once", side_effect=fake_once):
|
|
70
|
+
with patch.object(transport, "_ensure_mux"):
|
|
71
|
+
transport._ensure_compatible_daemon()
|
|
72
|
+
result = transport.call(
|
|
73
|
+
"env.open",
|
|
74
|
+
{"_env": env.to_agent_spec()},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.assertTrue(result["ready"])
|
|
78
|
+
bootstrap.assert_called_once_with(node)
|
|
79
|
+
self.assertIn("ping", calls)
|
|
80
|
+
self.assertIn("env.open", calls)
|
|
81
|
+
|
|
82
|
+
def test_version_mismatch_without_auto_bootstrap_is_actionable(self) -> None:
|
|
83
|
+
node = Node(
|
|
84
|
+
name="worker",
|
|
85
|
+
ssh_host="worker.example",
|
|
86
|
+
auto_bootstrap=False,
|
|
87
|
+
)
|
|
88
|
+
transport = SshRpcTransport(node, auto_bootstrap=False)
|
|
89
|
+
|
|
90
|
+
with patch.object(
|
|
91
|
+
transport,
|
|
92
|
+
"_call_once",
|
|
93
|
+
return_value={"ok": True, "version": "0.12.0"},
|
|
94
|
+
):
|
|
95
|
+
with self.assertRaisesRegex(RpcError, "version mismatch"):
|
|
96
|
+
transport._ensure_compatible_daemon()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
unittest.main()
|
|
@@ -298,6 +298,7 @@ class PackageSourceTest(unittest.TestCase):
|
|
|
298
298
|
node = Node(name="worker", ssh_host="worker")
|
|
299
299
|
transport = SshRpcTransport(node, auto_bootstrap=False)
|
|
300
300
|
transport._control_master = False
|
|
301
|
+
transport._compatible_daemon = True
|
|
301
302
|
transport._tunnel._local_port = 18080
|
|
302
303
|
chunks = []
|
|
303
304
|
|
|
@@ -392,6 +393,7 @@ class PackageSourceTest(unittest.TestCase):
|
|
|
392
393
|
def test_ssh_transport_streams_exec_events_and_returns_result(self):
|
|
393
394
|
node = Node(name="worker", ssh_host="worker")
|
|
394
395
|
transport = SshRpcTransport(node, auto_bootstrap=False)
|
|
396
|
+
transport._compatible_daemon = True
|
|
395
397
|
chunks = []
|
|
396
398
|
captured = {}
|
|
397
399
|
|
|
@@ -453,6 +455,7 @@ class PackageSourceTest(unittest.TestCase):
|
|
|
453
455
|
environment=environment,
|
|
454
456
|
auto_bootstrap=False,
|
|
455
457
|
)
|
|
458
|
+
transport._compatible_daemon = True
|
|
456
459
|
|
|
457
460
|
def fake_stream(args, **kwargs):
|
|
458
461
|
return subprocess.CompletedProcess(args, 2, b"", b"")
|
|
@@ -478,6 +481,7 @@ class PackageSourceTest(unittest.TestCase):
|
|
|
478
481
|
def test_streaming_rpc_rc_127_reports_daemon_unavailable(self):
|
|
479
482
|
node = Node(name="worker", ssh_host="worker")
|
|
480
483
|
transport = SshRpcTransport(node, auto_bootstrap=False)
|
|
484
|
+
transport._compatible_daemon = True
|
|
481
485
|
|
|
482
486
|
def fake_stream(args, **kwargs):
|
|
483
487
|
return subprocess.CompletedProcess(args, 127, b"", b"")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
7b3d2dcd31cc752b44289c5b0936569be7df1aef162d07c1b3c00e886cfea884 msdevd-linux-amd64
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
aeb5721e49746058e286ed14621208823ee092372c0a7d772cfdd9924d5b71e1 msdevd-linux-arm64
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|