hostctl 0.1.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.
- hostctl/AGENTS.md +292 -0
- hostctl/README.md +248 -0
- hostctl/__init__.py +194 -0
- hostctl/__main__.py +6 -0
- hostctl/_async.py +169 -0
- hostctl/_cli.py +232 -0
- hostctl/executor/__init__.py +82 -0
- hostctl/executor/_common.py +234 -0
- hostctl/executor/_qga.py +646 -0
- hostctl/executor/container.py +171 -0
- hostctl/executor/local.py +91 -0
- hostctl/executor/psrp.py +145 -0
- hostctl/executor/qemu.py +305 -0
- hostctl/executor/serial.py +312 -0
- hostctl/executor/ssh.py +226 -0
- hostctl/executor/winrm.py +255 -0
- hostctl/host/__init__.py +93 -0
- hostctl/host/_common.py +804 -0
- hostctl/host/_local.py +258 -0
- hostctl/host/_ssh.py +587 -0
- hostctl/host/_winrm.py +1002 -0
- hostctl/host/composite_path.py +567 -0
- hostctl/host/container.py +606 -0
- hostctl/host/container_path.py +664 -0
- hostctl/host/qemu.py +1078 -0
- hostctl/host/serial.py +401 -0
- hostctl/host/system.py +809 -0
- hostctl/process/__init__.py +49 -0
- hostctl/process/_common.py +113 -0
- hostctl/process/container.py +265 -0
- hostctl/process/psrp.py +208 -0
- hostctl/process/qemu_serial.py +247 -0
- hostctl/process/serial.py +257 -0
- hostctl/process/ssh.py +165 -0
- hostctl/provider/__init__.py +33 -0
- hostctl/provider/_common.py +452 -0
- hostctl/provider/transports.py +303 -0
- hostctl/py.typed +0 -0
- hostctl/serial/__init__.py +285 -0
- hostctl/shell/__init__.py +123 -0
- hostctl/shell/_common.py +616 -0
- hostctl/shell/cmd.py +167 -0
- hostctl/shell/fish.py +63 -0
- hostctl/shell/posix.py +83 -0
- hostctl/shell/powershell.py +120 -0
- hostctl/sync.py +245 -0
- hostctl-0.1.0.dist-info/METADATA +302 -0
- hostctl-0.1.0.dist-info/RECORD +51 -0
- hostctl-0.1.0.dist-info/WHEEL +4 -0
- hostctl-0.1.0.dist-info/entry_points.txt +2 -0
- hostctl-0.1.0.dist-info/licenses/LICENSE +21 -0
hostctl/host/_local.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Local host implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os as _os
|
|
6
|
+
import platform as _platform
|
|
7
|
+
import subprocess as _subprocess
|
|
8
|
+
import typing as _ty
|
|
9
|
+
|
|
10
|
+
from ..executor import LocalExecutor
|
|
11
|
+
from ..provider import OperationNotStarted, ProviderSelector
|
|
12
|
+
from ..provider.transports import LocalExecutorProvider, LocalPathProvider
|
|
13
|
+
from ._common import (
|
|
14
|
+
CaptureOutput,
|
|
15
|
+
Command,
|
|
16
|
+
Environment,
|
|
17
|
+
FileHandle,
|
|
18
|
+
Host,
|
|
19
|
+
HostConfig,
|
|
20
|
+
HostInfo,
|
|
21
|
+
HostPath,
|
|
22
|
+
Input,
|
|
23
|
+
PathLike,
|
|
24
|
+
starts_direct_command,
|
|
25
|
+
normalize_os_family,
|
|
26
|
+
strict_uri_credentials,
|
|
27
|
+
)
|
|
28
|
+
from ..shell import POSIX_SHELL, POWERSHELL, ShellFlavour
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LocalConfig(HostConfig, schemes=("local",)):
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def connection_uri(self) -> str:
|
|
37
|
+
return "local:"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _from_parsed_uri(cls, parsed, **credentials: object) -> LocalConfig:
|
|
41
|
+
strict_uri_credentials(credentials, ())
|
|
42
|
+
if parsed.netloc or parsed.path or parsed.query:
|
|
43
|
+
raise ValueError("local URI must be exactly 'local:'")
|
|
44
|
+
return cls()
|
|
45
|
+
|
|
46
|
+
def _create_host(self) -> LocalHost:
|
|
47
|
+
return LocalHost(self)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LocalHost(Host):
|
|
51
|
+
"""A host whose commands and paths are local to this process.
|
|
52
|
+
|
|
53
|
+
The public surface is unchanged, but execution and filesystem access are
|
|
54
|
+
assembled from ordered providers (:class:`LocalExecutorProvider` and
|
|
55
|
+
:class:`LocalPathProvider`) rather than a hard-wired executor. A subclass
|
|
56
|
+
may supply its own providers to reuse the local semantics over a different
|
|
57
|
+
access mechanism.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
config: _ty.Optional[LocalConfig] = None,
|
|
63
|
+
*,
|
|
64
|
+
executor_providers: _ty.Iterable[object] = (),
|
|
65
|
+
path_providers: _ty.Iterable[object] = (),
|
|
66
|
+
) -> None:
|
|
67
|
+
self.config = config or LocalConfig()
|
|
68
|
+
self._executor_provider_selector = ProviderSelector(
|
|
69
|
+
tuple(executor_providers) or (LocalExecutorProvider(),)
|
|
70
|
+
)
|
|
71
|
+
self._path_provider_selector = ProviderSelector(
|
|
72
|
+
tuple(path_providers) or (LocalPathProvider(),)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def executor_providers(self) -> _ty.Tuple[object, ...]:
|
|
77
|
+
"""The ordered command providers backing :meth:`run`."""
|
|
78
|
+
return self._executor_provider_selector.providers
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def path_providers(self) -> _ty.Tuple[object, ...]:
|
|
82
|
+
"""The ordered filesystem providers backing :meth:`path`."""
|
|
83
|
+
return self._path_provider_selector.providers
|
|
84
|
+
|
|
85
|
+
def _run_selector(self) -> ProviderSelector:
|
|
86
|
+
return self._executor_provider_selector
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def capabilities(self) -> _ty.FrozenSet[str]:
|
|
90
|
+
values = set()
|
|
91
|
+
if any(
|
|
92
|
+
self._executor_provider_selector.probe(provider).usable
|
|
93
|
+
for provider in self._executor_provider_selector.providers
|
|
94
|
+
):
|
|
95
|
+
values.add("run")
|
|
96
|
+
if any(
|
|
97
|
+
self._path_provider_selector.probe(provider).usable
|
|
98
|
+
for provider in self._path_provider_selector.providers
|
|
99
|
+
):
|
|
100
|
+
values.add("path")
|
|
101
|
+
return frozenset(values)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def shell_flavour(self) -> ShellFlavour:
|
|
105
|
+
if _os.name == "nt":
|
|
106
|
+
return POWERSHELL
|
|
107
|
+
if _os.name == "posix":
|
|
108
|
+
return POSIX_SHELL
|
|
109
|
+
raise NotImplementedError(f"unsupported local OS: {_os.name}")
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def executor(self) -> LocalExecutor:
|
|
113
|
+
return self._executor_provider_selector.select().provider.executor
|
|
114
|
+
|
|
115
|
+
def info(self) -> HostInfo:
|
|
116
|
+
return HostInfo(
|
|
117
|
+
hostname=_platform.node() or None,
|
|
118
|
+
os_family=normalize_os_family(_platform.system()),
|
|
119
|
+
os_name=_platform.system() or None,
|
|
120
|
+
os_version=_platform.version() or None,
|
|
121
|
+
architecture=_platform.machine() or None,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def path(self, *segments: PathLike, backend: _ty.Optional[str] = None) -> HostPath:
|
|
125
|
+
names = tuple(
|
|
126
|
+
provider.name for provider in self._path_provider_selector.providers
|
|
127
|
+
)
|
|
128
|
+
if backend is not None and backend not in names:
|
|
129
|
+
raise ValueError(
|
|
130
|
+
"local path backend must be "
|
|
131
|
+
+ " or ".join(repr(name) for name in names or ("local",))
|
|
132
|
+
)
|
|
133
|
+
if backend is None:
|
|
134
|
+
provider = self._path_provider_selector.select().provider
|
|
135
|
+
else:
|
|
136
|
+
provider = next(
|
|
137
|
+
item
|
|
138
|
+
for item in self._path_provider_selector.providers
|
|
139
|
+
if item.name == backend
|
|
140
|
+
)
|
|
141
|
+
if not provider.probe().usable:
|
|
142
|
+
raise OperationNotStarted(f"path provider {backend!r} is unavailable")
|
|
143
|
+
return provider.path(*(segments or (_os.getcwd(),)))
|
|
144
|
+
|
|
145
|
+
def run(
|
|
146
|
+
self,
|
|
147
|
+
*cmds: Command,
|
|
148
|
+
bufsize: int = -1,
|
|
149
|
+
executable: _ty.Optional[str] = None,
|
|
150
|
+
stdin: _ty.Optional[FileHandle] = None,
|
|
151
|
+
stdout: _ty.Optional[FileHandle] = None,
|
|
152
|
+
stderr: _ty.Optional[FileHandle] = None,
|
|
153
|
+
cwd: _ty.Optional[PathLike] = None,
|
|
154
|
+
env: _ty.Optional[Environment] = None,
|
|
155
|
+
capture_output: CaptureOutput = True,
|
|
156
|
+
check: bool = True,
|
|
157
|
+
encoding: _ty.Optional[str] = None,
|
|
158
|
+
errors: _ty.Optional[str] = None,
|
|
159
|
+
input: Input = None,
|
|
160
|
+
timeout: _ty.Optional[float] = None,
|
|
161
|
+
text: _ty.Optional[bool] = None,
|
|
162
|
+
) -> _subprocess.CompletedProcess:
|
|
163
|
+
excluded: _ty.List[str] = []
|
|
164
|
+
while True:
|
|
165
|
+
provider = self._executor_provider_selector.select(
|
|
166
|
+
exclude=excluded
|
|
167
|
+
).provider
|
|
168
|
+
try:
|
|
169
|
+
return self._run_with_provider(
|
|
170
|
+
provider,
|
|
171
|
+
cmds,
|
|
172
|
+
bufsize=bufsize,
|
|
173
|
+
executable=executable,
|
|
174
|
+
stdin=stdin,
|
|
175
|
+
stdout=stdout,
|
|
176
|
+
stderr=stderr,
|
|
177
|
+
cwd=cwd,
|
|
178
|
+
env=env,
|
|
179
|
+
capture_output=capture_output,
|
|
180
|
+
check=check,
|
|
181
|
+
encoding=encoding,
|
|
182
|
+
errors=errors,
|
|
183
|
+
input=input,
|
|
184
|
+
timeout=timeout,
|
|
185
|
+
text=text,
|
|
186
|
+
)
|
|
187
|
+
except OperationNotStarted as exc:
|
|
188
|
+
# Only a proven pre-dispatch refusal may reach another
|
|
189
|
+
# provider; a dispatched command is never replayed.
|
|
190
|
+
self._executor_provider_selector.decline(provider.name, str(exc))
|
|
191
|
+
excluded.append(provider.name)
|
|
192
|
+
|
|
193
|
+
def _run_with_provider(
|
|
194
|
+
self,
|
|
195
|
+
provider,
|
|
196
|
+
cmds: _ty.Sequence[Command],
|
|
197
|
+
*,
|
|
198
|
+
bufsize: int,
|
|
199
|
+
executable: _ty.Optional[str],
|
|
200
|
+
stdin,
|
|
201
|
+
stdout,
|
|
202
|
+
stderr,
|
|
203
|
+
cwd,
|
|
204
|
+
env,
|
|
205
|
+
capture_output,
|
|
206
|
+
check,
|
|
207
|
+
encoding,
|
|
208
|
+
errors,
|
|
209
|
+
input,
|
|
210
|
+
timeout,
|
|
211
|
+
text,
|
|
212
|
+
) -> _subprocess.CompletedProcess:
|
|
213
|
+
direct = starts_direct_command(cmds)
|
|
214
|
+
if direct is not None:
|
|
215
|
+
command, args = direct
|
|
216
|
+
if executable is not None:
|
|
217
|
+
raise NotImplementedError(
|
|
218
|
+
"executable cannot be combined with a direct command"
|
|
219
|
+
)
|
|
220
|
+
return provider.execute(
|
|
221
|
+
command,
|
|
222
|
+
*args,
|
|
223
|
+
bufsize=bufsize,
|
|
224
|
+
stdin=stdin,
|
|
225
|
+
stdout=stdout,
|
|
226
|
+
stderr=stderr,
|
|
227
|
+
cwd=cwd,
|
|
228
|
+
env=env,
|
|
229
|
+
capture_output=capture_output,
|
|
230
|
+
check=check,
|
|
231
|
+
encoding=encoding,
|
|
232
|
+
errors=errors,
|
|
233
|
+
input=input,
|
|
234
|
+
timeout=timeout,
|
|
235
|
+
text=text,
|
|
236
|
+
)
|
|
237
|
+
script = self.shell_flavour.script(cmds, cwd=None, env=None)
|
|
238
|
+
invocation = self.shell_flavour.invocation(
|
|
239
|
+
script,
|
|
240
|
+
executable=executable,
|
|
241
|
+
)
|
|
242
|
+
return provider.execute(
|
|
243
|
+
invocation[0],
|
|
244
|
+
*invocation[1:],
|
|
245
|
+
bufsize=bufsize,
|
|
246
|
+
stdin=stdin,
|
|
247
|
+
stdout=stdout,
|
|
248
|
+
stderr=stderr,
|
|
249
|
+
cwd=cwd,
|
|
250
|
+
env=env,
|
|
251
|
+
capture_output=capture_output,
|
|
252
|
+
check=check,
|
|
253
|
+
encoding=encoding,
|
|
254
|
+
errors=errors,
|
|
255
|
+
input=input,
|
|
256
|
+
timeout=timeout,
|
|
257
|
+
text=text,
|
|
258
|
+
)
|