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/executor/ssh.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""SSH command executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from ._common import (
|
|
12
|
+
CaptureOutput,
|
|
13
|
+
CommandArgument,
|
|
14
|
+
Environment,
|
|
15
|
+
Executor,
|
|
16
|
+
ExecutorCommand,
|
|
17
|
+
ExecutorCapability,
|
|
18
|
+
FileHandle,
|
|
19
|
+
Input,
|
|
20
|
+
dispatch_output,
|
|
21
|
+
normalize_environment,
|
|
22
|
+
normalize_input,
|
|
23
|
+
capture_streams,
|
|
24
|
+
reject_stdin_conflict,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _SshResult(typing.Protocol):
|
|
29
|
+
returncode: int
|
|
30
|
+
stdout: typing.Optional[typing.Union[str, bytes]]
|
|
31
|
+
stderr: typing.Optional[typing.Union[str, bytes]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SshConnection(typing.Protocol):
|
|
35
|
+
def is_closed(self) -> bool: ...
|
|
36
|
+
|
|
37
|
+
def close(self) -> None: ...
|
|
38
|
+
|
|
39
|
+
def wait_closed(self) -> typing.Awaitable[None]: ...
|
|
40
|
+
|
|
41
|
+
def run(self, command: str, **options: object) -> typing.Awaitable[_SshResult]: ...
|
|
42
|
+
|
|
43
|
+
def create_process(
|
|
44
|
+
self, command: typing.Optional[str] = None, **options: object
|
|
45
|
+
) -> typing.Awaitable[object]: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _input_buffer(
|
|
49
|
+
stream: FileHandle,
|
|
50
|
+
*,
|
|
51
|
+
encoding: typing.Optional[str],
|
|
52
|
+
errors: typing.Optional[str],
|
|
53
|
+
) -> io.BytesIO:
|
|
54
|
+
if stream == subprocess.DEVNULL:
|
|
55
|
+
return io.BytesIO()
|
|
56
|
+
if stream == subprocess.PIPE:
|
|
57
|
+
raise ValueError("stdin=subprocess.PIPE requires input")
|
|
58
|
+
if isinstance(stream, int):
|
|
59
|
+
with os.fdopen(os.dup(stream), "rb") as duplicate:
|
|
60
|
+
value = duplicate.read()
|
|
61
|
+
else:
|
|
62
|
+
value = stream.read()
|
|
63
|
+
if isinstance(value, str):
|
|
64
|
+
value = value.encode(encoding or "utf-8", errors or "strict")
|
|
65
|
+
return io.BytesIO(value)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SshExecutor(Executor[subprocess.CompletedProcess]):
|
|
69
|
+
"""Execute finalized command strings through an AsyncSSH connection."""
|
|
70
|
+
|
|
71
|
+
executor_capabilities: typing.FrozenSet[ExecutorCapability] = frozenset()
|
|
72
|
+
|
|
73
|
+
def __init__(self, connection: typing.Callable[[], SshConnection]) -> None:
|
|
74
|
+
self._connection = connection
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def _terminate_timeout(connection: SshConnection, error: BaseException) -> bool:
|
|
78
|
+
"""Best-effort termination for a timed-out AsyncSSH process.
|
|
79
|
+
|
|
80
|
+
``SSHClientConnection.run()`` does not expose the process it creates,
|
|
81
|
+
so AsyncSSH versions and test doubles may expose it on the exception or
|
|
82
|
+
connection. Probe those documented/duck-typed hooks and report
|
|
83
|
+
whether termination was attempted; the timeout remains marked
|
|
84
|
+
``orphaned`` when no channel can be recovered.
|
|
85
|
+
"""
|
|
86
|
+
targets = [
|
|
87
|
+
getattr(error, "process", None),
|
|
88
|
+
getattr(error, "channel", None),
|
|
89
|
+
getattr(connection, "process", None),
|
|
90
|
+
getattr(connection, "last_process", None),
|
|
91
|
+
]
|
|
92
|
+
target = next((item for item in targets if item is not None), None)
|
|
93
|
+
if target is None:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
from .. import _async
|
|
97
|
+
|
|
98
|
+
async def terminate() -> None:
|
|
99
|
+
for name in ("terminate", "close"):
|
|
100
|
+
method = getattr(target, name, None)
|
|
101
|
+
if method is None:
|
|
102
|
+
continue
|
|
103
|
+
result = method()
|
|
104
|
+
if inspect.isawaitable(result):
|
|
105
|
+
await result
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
_async.async_to_sync(terminate())
|
|
110
|
+
except Exception:
|
|
111
|
+
return False
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def __call__(
|
|
115
|
+
self,
|
|
116
|
+
command: ExecutorCommand,
|
|
117
|
+
*args: CommandArgument,
|
|
118
|
+
bufsize: int = -1,
|
|
119
|
+
stdin: typing.Optional[FileHandle] = None,
|
|
120
|
+
stdout: typing.Optional[FileHandle] = None,
|
|
121
|
+
stderr: typing.Optional[FileHandle] = None,
|
|
122
|
+
env: typing.Optional[Environment] = None,
|
|
123
|
+
capture_output: CaptureOutput = True,
|
|
124
|
+
check: bool = True,
|
|
125
|
+
encoding: typing.Optional[str] = None,
|
|
126
|
+
errors: typing.Optional[str] = None,
|
|
127
|
+
input: Input = None,
|
|
128
|
+
timeout: typing.Optional[float] = None,
|
|
129
|
+
text: typing.Optional[bool] = None,
|
|
130
|
+
**options: object,
|
|
131
|
+
) -> subprocess.CompletedProcess:
|
|
132
|
+
if options:
|
|
133
|
+
raise TypeError(f"unsupported SSH executor option: {sorted(options)[0]}")
|
|
134
|
+
if args:
|
|
135
|
+
raise NotImplementedError("SshExecutor does not support native arguments")
|
|
136
|
+
if bufsize == 0:
|
|
137
|
+
raise ValueError("bufsize=0 is unsupported by the buffered SSH executor")
|
|
138
|
+
command = str(command)
|
|
139
|
+
reject_stdin_conflict(input, stdin)
|
|
140
|
+
if text and encoding is None:
|
|
141
|
+
encoding = "utf-8"
|
|
142
|
+
env = normalize_environment(env)
|
|
143
|
+
|
|
144
|
+
from .. import _async
|
|
145
|
+
|
|
146
|
+
stdout, stderr = capture_streams(capture_output, stdout, stderr)
|
|
147
|
+
if input is not None:
|
|
148
|
+
# The sink is a BytesIO, so this leg is always binary regardless of
|
|
149
|
+
# the command's text encoding -- `text_mode=False` encodes str and
|
|
150
|
+
# passes bytes through.
|
|
151
|
+
value = normalize_input(
|
|
152
|
+
input, text_mode=False, encoding=encoding, errors=errors
|
|
153
|
+
)
|
|
154
|
+
stdin = io.BytesIO(typing.cast(bytes, value))
|
|
155
|
+
elif stdin is None:
|
|
156
|
+
# AsyncSSH leaves a command's stdin open when no stream is passed.
|
|
157
|
+
# Supplying an empty in-memory stream sends EOF and matches
|
|
158
|
+
# subprocess.run()'s non-interactive default.
|
|
159
|
+
stdin = _input_buffer(
|
|
160
|
+
subprocess.DEVNULL,
|
|
161
|
+
encoding=encoding,
|
|
162
|
+
errors=errors,
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
stdin = _input_buffer(
|
|
166
|
+
stdin,
|
|
167
|
+
encoding=encoding,
|
|
168
|
+
errors=errors,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
stdout_target, stderr_target = stdout, stderr
|
|
172
|
+
try:
|
|
173
|
+
result = _async.async_to_sync(
|
|
174
|
+
self._connection().run(
|
|
175
|
+
command,
|
|
176
|
+
bufsize=bufsize,
|
|
177
|
+
stdin=stdin,
|
|
178
|
+
stdout=None,
|
|
179
|
+
stderr=(
|
|
180
|
+
subprocess.STDOUT
|
|
181
|
+
if stderr_target == subprocess.STDOUT
|
|
182
|
+
else None
|
|
183
|
+
),
|
|
184
|
+
env=env,
|
|
185
|
+
check=False,
|
|
186
|
+
encoding=encoding,
|
|
187
|
+
errors=errors,
|
|
188
|
+
timeout=timeout,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
normalized = _async.normalize_asyncssh_error(
|
|
193
|
+
exc,
|
|
194
|
+
command=command,
|
|
195
|
+
timeout=timeout,
|
|
196
|
+
)
|
|
197
|
+
if isinstance(normalized, subprocess.TimeoutExpired):
|
|
198
|
+
terminated = self._terminate_timeout(self._connection(), exc)
|
|
199
|
+
normalized.orphaned = not terminated
|
|
200
|
+
if normalized is exc:
|
|
201
|
+
raise
|
|
202
|
+
raise normalized from exc
|
|
203
|
+
|
|
204
|
+
result_stdout = result.stdout
|
|
205
|
+
result_stderr = None if stderr_target == subprocess.STDOUT else result.stderr
|
|
206
|
+
result_stdout, result_stderr = dispatch_output(
|
|
207
|
+
stdout_target,
|
|
208
|
+
stderr_target,
|
|
209
|
+
result_stdout,
|
|
210
|
+
result_stderr,
|
|
211
|
+
encoding=encoding,
|
|
212
|
+
errors=errors,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
returncode = result.returncode
|
|
216
|
+
if returncode is None:
|
|
217
|
+
returncode = -1
|
|
218
|
+
completed = subprocess.CompletedProcess(
|
|
219
|
+
args=command,
|
|
220
|
+
returncode=returncode,
|
|
221
|
+
stdout=result_stdout,
|
|
222
|
+
stderr=result_stderr,
|
|
223
|
+
)
|
|
224
|
+
if check:
|
|
225
|
+
completed.check_returncode()
|
|
226
|
+
return completed
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""WinRM PowerShell script executor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import dataclasses
|
|
7
|
+
import subprocess
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
from ._common import (
|
|
11
|
+
CaptureOutput,
|
|
12
|
+
CommandArgument,
|
|
13
|
+
Executor,
|
|
14
|
+
ExecutorCommand,
|
|
15
|
+
ExecutorCapability,
|
|
16
|
+
FileHandle,
|
|
17
|
+
Input,
|
|
18
|
+
dispatch_output,
|
|
19
|
+
capture_streams,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _WinRMResponse(typing.Protocol):
|
|
24
|
+
status_code: int
|
|
25
|
+
std_out: bytes
|
|
26
|
+
std_err: bytes
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WinRMSession(typing.Protocol):
|
|
30
|
+
def run_ps(self, script: str) -> _WinRMResponse: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclasses.dataclass
|
|
34
|
+
class _NativeResponse:
|
|
35
|
+
status_code: int
|
|
36
|
+
std_out: bytes
|
|
37
|
+
std_err: bytes
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class NativeWinRMSession:
|
|
41
|
+
"""Current-context Windows PowerShell remoting session adapter."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
host: str,
|
|
46
|
+
*,
|
|
47
|
+
ssl: bool = False,
|
|
48
|
+
port: typing.Optional[int] = None,
|
|
49
|
+
timeout: typing.Optional[float] = None,
|
|
50
|
+
transport: str = "ntlm",
|
|
51
|
+
server_cert_validation: str = "validate",
|
|
52
|
+
message_encryption: str = "auto",
|
|
53
|
+
) -> None:
|
|
54
|
+
self.host = host
|
|
55
|
+
self.ssl = ssl
|
|
56
|
+
self.port = port
|
|
57
|
+
self.timeout = timeout
|
|
58
|
+
if transport not in {"ntlm", "kerberos"}:
|
|
59
|
+
raise NotImplementedError(
|
|
60
|
+
"native WinRM supports only current-context Negotiate (ntlm/kerberos)"
|
|
61
|
+
)
|
|
62
|
+
if message_encryption not in {"auto", "always", "never"}:
|
|
63
|
+
raise ValueError("invalid message_encryption")
|
|
64
|
+
if message_encryption != "auto":
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
"native WinRM cannot override WS-Man message encryption; "
|
|
67
|
+
"use message_encryption='auto' or the pywinrm provider"
|
|
68
|
+
)
|
|
69
|
+
if server_cert_validation not in {"validate", "ignore"}:
|
|
70
|
+
raise ValueError("invalid server_cert_validation")
|
|
71
|
+
if ssl and server_cert_validation == "ignore":
|
|
72
|
+
# PowerShell remoting can skip certificate checks, but only when
|
|
73
|
+
# explicitly requested; keep this visible in the generated options.
|
|
74
|
+
self._skip_ca_check = True
|
|
75
|
+
else:
|
|
76
|
+
self._skip_ca_check = False
|
|
77
|
+
|
|
78
|
+
def run_ps(self, script: str) -> _NativeResponse:
|
|
79
|
+
host = base64.b64encode(self.host.encode("utf-8")).decode("ascii")
|
|
80
|
+
payload = base64.b64encode(script.encode("utf-8")).decode("ascii")
|
|
81
|
+
wrapper = (
|
|
82
|
+
"$OutputEncoding=[Console]::OutputEncoding="
|
|
83
|
+
"[Text.UTF8Encoding]::new($false);"
|
|
84
|
+
"$ErrorActionPreference='Stop';"
|
|
85
|
+
"$h=[Text.Encoding]::UTF8.GetString("
|
|
86
|
+
f"[Convert]::FromBase64String('{host}'));"
|
|
87
|
+
"$s=[Text.Encoding]::UTF8.GetString("
|
|
88
|
+
f"[Convert]::FromBase64String('{payload}'));"
|
|
89
|
+
"$o=@{ComputerName=$h;Authentication='Negotiate';"
|
|
90
|
+
f"UseSSL=${str(self.ssl).lower()}"
|
|
91
|
+
+ (f";Port={self.port}" if self.port is not None else "")
|
|
92
|
+
+ "};"
|
|
93
|
+
"try{$global:LASTEXITCODE=0;"
|
|
94
|
+
"$r=Invoke-Command @o -ScriptBlock ([ScriptBlock]::Create($s));"
|
|
95
|
+
"$r;exit ([int]$global:LASTEXITCODE)}"
|
|
96
|
+
"catch{$c=[string]$_.CategoryInfo.Category;"
|
|
97
|
+
"$m=[Convert]::ToBase64String("
|
|
98
|
+
"[Text.Encoding]::UTF8.GetBytes($_.Exception.Message));"
|
|
99
|
+
"[Console]::Error.Write('HOSTCTL_NATIVE_ERROR:'+$c+':'+$m);exit 1}"
|
|
100
|
+
)
|
|
101
|
+
if self._skip_ca_check:
|
|
102
|
+
wrapper = wrapper.replace(
|
|
103
|
+
";Port=" + str(self.port) if self.port is not None else ";};",
|
|
104
|
+
(
|
|
105
|
+
";SkipCACheck=$true;Port=" + str(self.port)
|
|
106
|
+
if self.port is not None
|
|
107
|
+
else ";SkipCACheck=$true;};"
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
try:
|
|
111
|
+
result = subprocess.run(
|
|
112
|
+
(
|
|
113
|
+
"powershell.exe",
|
|
114
|
+
"-NoProfile",
|
|
115
|
+
"-NonInteractive",
|
|
116
|
+
"-Command",
|
|
117
|
+
"-",
|
|
118
|
+
),
|
|
119
|
+
input=wrapper.encode("utf-8"),
|
|
120
|
+
stdout=subprocess.PIPE,
|
|
121
|
+
stderr=subprocess.PIPE,
|
|
122
|
+
check=False,
|
|
123
|
+
timeout=self.timeout,
|
|
124
|
+
)
|
|
125
|
+
except subprocess.TimeoutExpired as exc:
|
|
126
|
+
# Never expose the local powershell argv as the remote command.
|
|
127
|
+
raise subprocess.TimeoutExpired(self.host, self.timeout) from exc
|
|
128
|
+
return _NativeResponse(result.returncode, result.stdout, result.stderr)
|
|
129
|
+
|
|
130
|
+
def close(self) -> None:
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WinRMExecutor(Executor[subprocess.CompletedProcess]):
|
|
135
|
+
"""Execute finalized PowerShell scripts through a pywinrm session."""
|
|
136
|
+
|
|
137
|
+
executor_capabilities = frozenset((ExecutorCapability.SCRIPT,))
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
session: typing.Callable[[], WinRMSession],
|
|
142
|
+
transport_timeout: typing.Optional[typing.Callable[[], float]] = None,
|
|
143
|
+
) -> None:
|
|
144
|
+
self._session = session
|
|
145
|
+
self._transport_timeout = transport_timeout
|
|
146
|
+
|
|
147
|
+
def __call__(
|
|
148
|
+
self,
|
|
149
|
+
command: ExecutorCommand,
|
|
150
|
+
*args: CommandArgument,
|
|
151
|
+
bufsize: int = -1,
|
|
152
|
+
stdin: typing.Optional[FileHandle] = None,
|
|
153
|
+
stdout: typing.Optional[FileHandle] = None,
|
|
154
|
+
stderr: typing.Optional[FileHandle] = None,
|
|
155
|
+
capture_output: CaptureOutput = True,
|
|
156
|
+
check: bool = True,
|
|
157
|
+
encoding: typing.Optional[str] = None,
|
|
158
|
+
errors: typing.Optional[str] = None,
|
|
159
|
+
input: Input = None,
|
|
160
|
+
timeout: typing.Optional[float] = None,
|
|
161
|
+
text: typing.Optional[bool] = None,
|
|
162
|
+
**options: object,
|
|
163
|
+
) -> subprocess.CompletedProcess:
|
|
164
|
+
if options:
|
|
165
|
+
raise TypeError(f"unsupported WinRM executor option: {sorted(options)[0]}")
|
|
166
|
+
if args:
|
|
167
|
+
raise NotImplementedError("WinRMExecutor does not support native arguments")
|
|
168
|
+
command = str(command)
|
|
169
|
+
if timeout is not None:
|
|
170
|
+
raise NotImplementedError(
|
|
171
|
+
"WinRMExecutor timeout is unsupported; configure transport timeouts"
|
|
172
|
+
)
|
|
173
|
+
if stdin is not None or input is not None:
|
|
174
|
+
raise NotImplementedError("WinRMExecutor does not support stdin/input")
|
|
175
|
+
stdout, stderr = capture_streams(capture_output, stdout, stderr)
|
|
176
|
+
try:
|
|
177
|
+
result = self._session().run_ps(command)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
normalized = self._normalize_error(exc, command)
|
|
180
|
+
if normalized is exc:
|
|
181
|
+
raise
|
|
182
|
+
raise normalized from exc
|
|
183
|
+
out = result.std_out
|
|
184
|
+
err = result.std_err
|
|
185
|
+
marker = b"HOSTCTL_NATIVE_ERROR:"
|
|
186
|
+
if result.status_code and err.startswith(marker):
|
|
187
|
+
try:
|
|
188
|
+
_, category, encoded = err.decode("ascii").split(":", 2)
|
|
189
|
+
detail = base64.b64decode(encoded).decode("utf-8", "replace")
|
|
190
|
+
except Exception:
|
|
191
|
+
category, detail = "transport", err.decode("utf-8", "replace")
|
|
192
|
+
if category in ("AuthenticationError", "PermissionDenied"):
|
|
193
|
+
raise PermissionError(detail)
|
|
194
|
+
if category not in ("RemoteError", "Remote"):
|
|
195
|
+
raise ConnectionError(detail)
|
|
196
|
+
# Remote errors are represented as a normal non-zero completion;
|
|
197
|
+
# check=False must be able to inspect them.
|
|
198
|
+
err = detail.encode(encoding or "utf-8", errors or "replace")
|
|
199
|
+
if text or encoding is not None or errors is not None:
|
|
200
|
+
codec = encoding or "utf-8"
|
|
201
|
+
out = out.decode(codec, errors or "strict")
|
|
202
|
+
err = err.decode(codec, errors or "strict")
|
|
203
|
+
|
|
204
|
+
if stderr is subprocess.STDOUT:
|
|
205
|
+
if out is not None:
|
|
206
|
+
out += err
|
|
207
|
+
err = None
|
|
208
|
+
out, err = dispatch_output(
|
|
209
|
+
stdout,
|
|
210
|
+
stderr,
|
|
211
|
+
out,
|
|
212
|
+
err,
|
|
213
|
+
encoding=encoding,
|
|
214
|
+
errors=errors,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
completed = subprocess.CompletedProcess(
|
|
218
|
+
args=command,
|
|
219
|
+
returncode=result.status_code,
|
|
220
|
+
stdout=out,
|
|
221
|
+
stderr=err,
|
|
222
|
+
)
|
|
223
|
+
if check:
|
|
224
|
+
completed.check_returncode()
|
|
225
|
+
return completed
|
|
226
|
+
|
|
227
|
+
def _normalize_error(self, exc: Exception, command: str) -> Exception:
|
|
228
|
+
try:
|
|
229
|
+
import requests
|
|
230
|
+
import winrm.exceptions
|
|
231
|
+
except ImportError:
|
|
232
|
+
return exc
|
|
233
|
+
if isinstance(exc, winrm.exceptions.AuthenticationError):
|
|
234
|
+
return PermissionError(str(exc))
|
|
235
|
+
if isinstance(
|
|
236
|
+
exc,
|
|
237
|
+
(
|
|
238
|
+
TimeoutError,
|
|
239
|
+
subprocess.TimeoutExpired,
|
|
240
|
+
requests.exceptions.Timeout,
|
|
241
|
+
winrm.exceptions.WinRMOperationTimeoutError,
|
|
242
|
+
),
|
|
243
|
+
):
|
|
244
|
+
timeout = self._transport_timeout() if self._transport_timeout else None
|
|
245
|
+
return subprocess.TimeoutExpired(command, timeout)
|
|
246
|
+
if isinstance(
|
|
247
|
+
exc,
|
|
248
|
+
(
|
|
249
|
+
requests.exceptions.ConnectionError,
|
|
250
|
+
winrm.exceptions.WinRMTransportError,
|
|
251
|
+
winrm.exceptions.WSManFaultError,
|
|
252
|
+
),
|
|
253
|
+
):
|
|
254
|
+
return ConnectionError(str(exc))
|
|
255
|
+
return exc
|
hostctl/host/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Host contracts and built-in host implementations."""
|
|
2
|
+
|
|
3
|
+
from ._common import (
|
|
4
|
+
Host as Host,
|
|
5
|
+
HostConfig as HostConfig,
|
|
6
|
+
HostInfo as HostInfo,
|
|
7
|
+
HostPath as HostPath,
|
|
8
|
+
parse_credentials as parse_credentials,
|
|
9
|
+
redact_uri as redact_uri,
|
|
10
|
+
strict_uri_credentials as strict_uri_credentials,
|
|
11
|
+
strict_uri_query as strict_uri_query,
|
|
12
|
+
uri_host as uri_host,
|
|
13
|
+
)
|
|
14
|
+
from ._ssh import ssh_providers as ssh_providers
|
|
15
|
+
from ._winrm import winrm_providers as winrm_providers
|
|
16
|
+
from ._local import LocalConfig as LocalConfig, LocalHost as LocalHost
|
|
17
|
+
from .container import (
|
|
18
|
+
ContainerConfig as ContainerConfig,
|
|
19
|
+
ContainerHost as ContainerHost,
|
|
20
|
+
)
|
|
21
|
+
from .container_path import (
|
|
22
|
+
ContainerPathBackend as ContainerPathBackend,
|
|
23
|
+
PosixContainerPath as PosixContainerPath,
|
|
24
|
+
WindowsContainerPath as WindowsContainerPath,
|
|
25
|
+
)
|
|
26
|
+
from .qemu import (
|
|
27
|
+
QemuConfig as QemuConfig,
|
|
28
|
+
QemuHost as QemuHost,
|
|
29
|
+
PosixQemuPath as PosixQemuPath,
|
|
30
|
+
QgaPathBackend as QgaPathBackend,
|
|
31
|
+
WindowsQemuPath as WindowsQemuPath,
|
|
32
|
+
)
|
|
33
|
+
from ._ssh import (
|
|
34
|
+
PathnameConstructor as PathnameConstructor,
|
|
35
|
+
SshConfig as SshConfig,
|
|
36
|
+
)
|
|
37
|
+
from ._winrm import (
|
|
38
|
+
WinRMConfig as WinRMConfig,
|
|
39
|
+
WinRMPath as WinRMPath,
|
|
40
|
+
WinRMPathBackend as WinRMPathBackend,
|
|
41
|
+
)
|
|
42
|
+
from .system import (
|
|
43
|
+
IosConfig as IosConfig,
|
|
44
|
+
IosHost as IosHost,
|
|
45
|
+
PosixConfig as PosixConfig,
|
|
46
|
+
PosixHost as PosixHost,
|
|
47
|
+
SystemConfig as SystemConfig,
|
|
48
|
+
SystemHost as SystemHost,
|
|
49
|
+
WindowsConfig as WindowsConfig,
|
|
50
|
+
WindowsHost as WindowsHost,
|
|
51
|
+
register_system_provider as register_system_provider,
|
|
52
|
+
)
|
|
53
|
+
from .serial import SerialConfig as SerialConfig, SerialHost as SerialHost
|
|
54
|
+
from .composite_path import (
|
|
55
|
+
CompositePosixPath as CompositePosixPath,
|
|
56
|
+
CompositeWindowsPath as CompositeWindowsPath,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"Host",
|
|
61
|
+
"HostConfig",
|
|
62
|
+
"HostInfo",
|
|
63
|
+
"parse_credentials",
|
|
64
|
+
"redact_uri",
|
|
65
|
+
"ssh_providers",
|
|
66
|
+
"strict_uri_credentials",
|
|
67
|
+
"strict_uri_query",
|
|
68
|
+
"uri_host",
|
|
69
|
+
"winrm_providers",
|
|
70
|
+
"HostPath",
|
|
71
|
+
"ContainerConfig",
|
|
72
|
+
"ContainerHost",
|
|
73
|
+
"LocalConfig",
|
|
74
|
+
"LocalHost",
|
|
75
|
+
"QemuConfig",
|
|
76
|
+
"QemuHost",
|
|
77
|
+
"SshConfig",
|
|
78
|
+
"SerialConfig",
|
|
79
|
+
"SerialHost",
|
|
80
|
+
"WinRMConfig",
|
|
81
|
+
"WinRMPath",
|
|
82
|
+
"SystemConfig",
|
|
83
|
+
"SystemHost",
|
|
84
|
+
"PosixConfig",
|
|
85
|
+
"PosixHost",
|
|
86
|
+
"WindowsConfig",
|
|
87
|
+
"WindowsHost",
|
|
88
|
+
"register_system_provider",
|
|
89
|
+
"IosConfig",
|
|
90
|
+
"IosHost",
|
|
91
|
+
"CompositePosixPath",
|
|
92
|
+
"CompositeWindowsPath",
|
|
93
|
+
]
|