msdev 0.9.0__py3-none-any.whl
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/__init__.py +3 -0
- msdev/cli.py +1460 -0
- msdev/core/__init__.py +1 -0
- msdev/core/config.py +193 -0
- msdev/core/guides.py +100 -0
- msdev/core/inventory.py +1390 -0
- msdev/core/limits.py +44 -0
- msdev/core/resources.py +300 -0
- msdev/core/services/__init__.py +111 -0
- msdev/core/services/context.py +61 -0
- msdev/core/services/environment.py +84 -0
- msdev/core/services/execution.py +136 -0
- msdev/core/services/model.py +1085 -0
- msdev/core/services/node.py +210 -0
- msdev/core/services/npu.py +116 -0
- msdev/core/services/workspace.py +567 -0
- msdev/core/transport.py +1225 -0
- msdev/core/workspace/__init__.py +36 -0
- msdev/core/workspace/access.py +1216 -0
- msdev/core/workspace/client.py +274 -0
- msdev/core/workspace/paths.py +45 -0
- msdev/core/workspace/registry.py +151 -0
- msdev/daemon.py +1322 -0
- msdev/session/__init__.py +13 -0
- msdev/session/export.py +190 -0
- msdev/session/log.py +269 -0
- msdev-0.9.0.dist-info/METADATA +295 -0
- msdev-0.9.0.dist-info/RECORD +31 -0
- msdev-0.9.0.dist-info/WHEEL +5 -0
- msdev-0.9.0.dist-info/entry_points.txt +3 -0
- msdev-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Node registry and SSH lifecycle application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..resources import Node, parse_variable_assignment
|
|
10
|
+
from ..transport import RpcError
|
|
11
|
+
from .context import ServiceContext
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class NodeAddRequest:
|
|
16
|
+
name: str
|
|
17
|
+
ssh_host: str | None = None
|
|
18
|
+
remote_bin: str = "~/.local/bin/msdevd"
|
|
19
|
+
auto_bootstrap: bool = True
|
|
20
|
+
force: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class NodeListResult:
|
|
25
|
+
nodes: tuple[Node, ...]
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, Any]:
|
|
28
|
+
return {"nodes": [asdict(node) for node in self.nodes]}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class NodeConnectionResult:
|
|
33
|
+
node: str
|
|
34
|
+
ssh_host: str
|
|
35
|
+
state: str | None = None
|
|
36
|
+
persist: str | None = None
|
|
37
|
+
error: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class NodeConnectionBatch:
|
|
42
|
+
action: str
|
|
43
|
+
items: tuple[NodeConnectionResult, ...]
|
|
44
|
+
exit_code: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NodeService:
|
|
48
|
+
def __init__(self, context: ServiceContext):
|
|
49
|
+
self.context = context
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _required(value: str | None, label: str) -> str:
|
|
53
|
+
if not isinstance(value, str) or not value.strip():
|
|
54
|
+
raise ValueError(f"{label} must be a non-empty string")
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def _boolean(value: Any, label: str) -> bool:
|
|
59
|
+
if type(value) is not bool:
|
|
60
|
+
raise ValueError(f"{label} must be a boolean")
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
def add(self, request: NodeAddRequest) -> Node:
|
|
64
|
+
name = self._required(request.name, "node name")
|
|
65
|
+
node = Node(
|
|
66
|
+
name=name,
|
|
67
|
+
ssh_host=self._required(request.ssh_host or name, "SSH host"),
|
|
68
|
+
remote_bin=self._required(request.remote_bin, "remote binary"),
|
|
69
|
+
auto_bootstrap=self._boolean(
|
|
70
|
+
request.auto_bootstrap,
|
|
71
|
+
"auto_bootstrap",
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
force = self._boolean(request.force, "force")
|
|
75
|
+
transport = self.context.create_transport(node)
|
|
76
|
+
validate = getattr(transport, "validate", None)
|
|
77
|
+
if validate is None:
|
|
78
|
+
raise TypeError("node transport does not support SSH validation")
|
|
79
|
+
validate()
|
|
80
|
+
self.context.node_store.add(
|
|
81
|
+
node,
|
|
82
|
+
force=force,
|
|
83
|
+
create_host_environment=True,
|
|
84
|
+
)
|
|
85
|
+
return node
|
|
86
|
+
|
|
87
|
+
def list(self) -> NodeListResult:
|
|
88
|
+
return NodeListResult(tuple(self.context.node_store.list()))
|
|
89
|
+
|
|
90
|
+
def update_variables(
|
|
91
|
+
self,
|
|
92
|
+
name: str,
|
|
93
|
+
*,
|
|
94
|
+
set_values: tuple[str, ...] = (),
|
|
95
|
+
unset_keys: tuple[str, ...] = (),
|
|
96
|
+
) -> Node:
|
|
97
|
+
node_name = self._required(name, "node name")
|
|
98
|
+
updates: dict[str, str] = {}
|
|
99
|
+
for assignment in set_values:
|
|
100
|
+
key, value = parse_variable_assignment(assignment)
|
|
101
|
+
updates[key] = value
|
|
102
|
+
for key in unset_keys:
|
|
103
|
+
parsed_key, _ = parse_variable_assignment(f"{key}=")
|
|
104
|
+
if parsed_key in updates:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"environment variable cannot be set and unset: {key}"
|
|
107
|
+
)
|
|
108
|
+
return self.context.node_store.update_variables(
|
|
109
|
+
node_name,
|
|
110
|
+
set_values=updates,
|
|
111
|
+
unset_keys=unset_keys,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def remove(self, name: str, *, cascade: bool = False) -> tuple[str, ...]:
|
|
115
|
+
node_name = self._required(name, "node name")
|
|
116
|
+
self._boolean(cascade, "cascade")
|
|
117
|
+
return tuple(self.context.node_store.remove(node_name, cascade=cascade))
|
|
118
|
+
|
|
119
|
+
def status(self, name: str) -> dict[str, Any]:
|
|
120
|
+
node_name = self._required(name, "node name")
|
|
121
|
+
result = self.context.transport_for_node(node_name).call("node.info")
|
|
122
|
+
if not isinstance(result, dict):
|
|
123
|
+
raise RuntimeError("node.info returned a non-object result")
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def bootstrap(
|
|
127
|
+
self,
|
|
128
|
+
name: str,
|
|
129
|
+
package_path: Path | None = None,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
node = self.context.node_store.get(self._required(name, "node name"))
|
|
132
|
+
return self.context.bootstrap(node, package_path)
|
|
133
|
+
|
|
134
|
+
def _connection_nodes(
|
|
135
|
+
self,
|
|
136
|
+
name: str | None,
|
|
137
|
+
all_nodes: bool,
|
|
138
|
+
) -> list[Node]:
|
|
139
|
+
self._boolean(all_nodes, "all_nodes")
|
|
140
|
+
if all_nodes and name:
|
|
141
|
+
raise ValueError("node name and --all cannot be used together")
|
|
142
|
+
if all_nodes:
|
|
143
|
+
nodes = self.context.node_store.list()
|
|
144
|
+
if not nodes:
|
|
145
|
+
raise ValueError("no SSH nodes registered")
|
|
146
|
+
return nodes
|
|
147
|
+
if not name:
|
|
148
|
+
raise ValueError("node name or --all is required")
|
|
149
|
+
return [self.context.node_store.get(self._required(name, "node name"))]
|
|
150
|
+
|
|
151
|
+
def _connection(
|
|
152
|
+
self,
|
|
153
|
+
action: str,
|
|
154
|
+
name: str | None,
|
|
155
|
+
all_nodes: bool,
|
|
156
|
+
) -> NodeConnectionBatch:
|
|
157
|
+
items: list[NodeConnectionResult] = []
|
|
158
|
+
failed = False
|
|
159
|
+
for node in self._connection_nodes(name, all_nodes):
|
|
160
|
+
operation = getattr(self.context.create_transport(node), action)
|
|
161
|
+
try:
|
|
162
|
+
result = operation()
|
|
163
|
+
if action == "connect":
|
|
164
|
+
state = (
|
|
165
|
+
"already connected"
|
|
166
|
+
if result["already_connected"]
|
|
167
|
+
else "connected"
|
|
168
|
+
)
|
|
169
|
+
persist = str(result["persist"])
|
|
170
|
+
else:
|
|
171
|
+
state = (
|
|
172
|
+
"already disconnected"
|
|
173
|
+
if result["already_disconnected"]
|
|
174
|
+
else "disconnected"
|
|
175
|
+
)
|
|
176
|
+
persist = None
|
|
177
|
+
items.append(
|
|
178
|
+
NodeConnectionResult(
|
|
179
|
+
node.name,
|
|
180
|
+
node.ssh_host,
|
|
181
|
+
state,
|
|
182
|
+
persist,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
except (RpcError, OSError) as exc:
|
|
186
|
+
failed = True
|
|
187
|
+
items.append(
|
|
188
|
+
NodeConnectionResult(
|
|
189
|
+
node.name,
|
|
190
|
+
node.ssh_host,
|
|
191
|
+
error=str(exc),
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
return NodeConnectionBatch(action, tuple(items), 1 if failed else 0)
|
|
195
|
+
|
|
196
|
+
def connect(
|
|
197
|
+
self,
|
|
198
|
+
name: str | None = None,
|
|
199
|
+
*,
|
|
200
|
+
all_nodes: bool = False,
|
|
201
|
+
) -> NodeConnectionBatch:
|
|
202
|
+
return self._connection("connect", name, all_nodes)
|
|
203
|
+
|
|
204
|
+
def disconnect(
|
|
205
|
+
self,
|
|
206
|
+
name: str | None = None,
|
|
207
|
+
*,
|
|
208
|
+
all_nodes: bool = False,
|
|
209
|
+
) -> NodeConnectionBatch:
|
|
210
|
+
return self._connection("disconnect", name, all_nodes)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""NPU discovery application service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..transport import RpcError
|
|
9
|
+
from .context import ServiceContext
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class NpuNodeResult:
|
|
14
|
+
node: str
|
|
15
|
+
ssh_host: str | None
|
|
16
|
+
result: dict[str, Any] | None = None
|
|
17
|
+
error: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class NpuListResult:
|
|
22
|
+
items: tuple[NpuNodeResult, ...]
|
|
23
|
+
all_nodes: bool
|
|
24
|
+
exit_code: int
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict[str, Any]:
|
|
27
|
+
if not self.all_nodes:
|
|
28
|
+
item = self.items[0]
|
|
29
|
+
return {
|
|
30
|
+
"node": item.node,
|
|
31
|
+
**(item.result or {}),
|
|
32
|
+
"exit_code": self.exit_code,
|
|
33
|
+
}
|
|
34
|
+
nodes: dict[str, Any] = {}
|
|
35
|
+
for item in self.items:
|
|
36
|
+
value: dict[str, Any] = {"ssh_host": item.ssh_host}
|
|
37
|
+
if item.error is not None:
|
|
38
|
+
value["error"] = item.error
|
|
39
|
+
else:
|
|
40
|
+
value["result"] = item.result
|
|
41
|
+
nodes[item.node] = value
|
|
42
|
+
return {"nodes": nodes, "exit_code": self.exit_code}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class NpuService:
|
|
46
|
+
def __init__(self, context: ServiceContext):
|
|
47
|
+
self.context = context
|
|
48
|
+
|
|
49
|
+
def list(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
node: str | None = None,
|
|
53
|
+
all_nodes: bool = False,
|
|
54
|
+
) -> NpuListResult:
|
|
55
|
+
if type(all_nodes) is not bool:
|
|
56
|
+
raise ValueError("all_nodes must be a boolean")
|
|
57
|
+
if node is not None and not isinstance(node, str):
|
|
58
|
+
raise ValueError("node must be a non-empty string")
|
|
59
|
+
node_name = node if isinstance(node, str) and node.strip() else ""
|
|
60
|
+
if all_nodes and node_name:
|
|
61
|
+
raise ValueError("node and all_nodes cannot be used together")
|
|
62
|
+
if all_nodes:
|
|
63
|
+
configured = self.context.node_store.list()
|
|
64
|
+
if not configured:
|
|
65
|
+
raise ValueError("no SSH nodes registered")
|
|
66
|
+
items: list[NpuNodeResult] = []
|
|
67
|
+
failed = False
|
|
68
|
+
for configured_node in configured:
|
|
69
|
+
try:
|
|
70
|
+
result = self.context.create_transport(
|
|
71
|
+
configured_node
|
|
72
|
+
).call("npu.list")
|
|
73
|
+
if not isinstance(result, dict):
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"npu.list returned a non-object result"
|
|
76
|
+
)
|
|
77
|
+
items.append(
|
|
78
|
+
NpuNodeResult(
|
|
79
|
+
node=configured_node.name,
|
|
80
|
+
ssh_host=configured_node.ssh_host,
|
|
81
|
+
result=result,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
if not result.get("available"):
|
|
85
|
+
failed = True
|
|
86
|
+
except (RpcError, OSError) as exc:
|
|
87
|
+
failed = True
|
|
88
|
+
items.append(
|
|
89
|
+
NpuNodeResult(
|
|
90
|
+
node=configured_node.name,
|
|
91
|
+
ssh_host=configured_node.ssh_host,
|
|
92
|
+
error=str(exc),
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return NpuListResult(
|
|
96
|
+
items=tuple(items),
|
|
97
|
+
all_nodes=True,
|
|
98
|
+
exit_code=1 if failed else 0,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if not node_name:
|
|
102
|
+
raise ValueError("node is required unless all_nodes is true")
|
|
103
|
+
result = self.context.transport_for_node(node_name).call("npu.list")
|
|
104
|
+
if not isinstance(result, dict):
|
|
105
|
+
raise RuntimeError("npu.list returned a non-object result")
|
|
106
|
+
return NpuListResult(
|
|
107
|
+
items=(
|
|
108
|
+
NpuNodeResult(
|
|
109
|
+
node=node_name,
|
|
110
|
+
ssh_host=None,
|
|
111
|
+
result=result,
|
|
112
|
+
),
|
|
113
|
+
),
|
|
114
|
+
all_nodes=False,
|
|
115
|
+
exit_code=0 if result.get("available") else 1,
|
|
116
|
+
)
|