API Reference
The names below are the public exports from hostctl.__all__ — everything a
caller has to be able to write: a host or config to construct, an exception
to catch, a type to annotate with, an argument to pass, or a contract to
implement.
Each name also has a public home module: hostctl.host, hostctl.exceptions,
hostctl.uri, hostctl.system, hostctl.provider, hostctl.executor,
hostctl.process, hostctl.paths, hostctl.shell, hostctl.sync,
hostctl.console, hostctl.cli and the transport packages
hostctl.transports.local, .ssh, .winrm, .container, .qemu and
.serial. Each declares __all__; a name
outside it, and every module whose name starts with an underscore, is internal
and may change without a deprecation cycle. Concrete executors, process
adapters and path backends are internal: a host hands them back typed as
pathapi.Path, Process or Executor.
Hosts and configuration
hostctl.Host(*args, **secrets)
Host()
Host(connection_string: str, /, **secrets: str)
Bases: ABC
Protocol-independent operational interface to a machine.
Source code in src/hostctl/host/_host.py
123 124 | |
capabilities
property
Operations supported by this host.
config
instance-attribute
connection_uri
property
executor
property
The command executor used when binding this host's shell.
executor_capabilities
property
Native context/argument features of the underlying executor.
Derived from the host's own executor so a host that does not compose
providers still reports truthfully. Host.executor's default wrapper
reads this property, so it is skipped here to avoid recursing; a host
that overrides executor with a real executor reports that executor's
capabilities.
last_selection
property
The redacted provider trace for the most recent :meth:run.
The run-side counterpart of CompositePosixPath.selection_trace:
:class:~hostctl.provider.SelectionStep records in provider
precedence order.
The trace accumulates across the failover attempts of a single
run(), so a call that fell through from one provider to another
reports every provider tried and every refusal reason -- including on
the very call that suffered them.
Empty for a host whose run() does not select between providers
(QEMUHost, SerialHost), and empty before the first run().
Values are redacted on the way in; see
:meth:~hostctl.provider.ProviderSelector.redact for the limits of
that guarantee.
logger
property
writable
Where records about this host go: by default an adapter over a
hostctl logger that prefixes [name] and sets record.host.
Assign a Logger or LoggerAdapter to send them elsewhere; a
provider fallback's warning is logged here.
name
property
writable
A short label for this target, for log lines: the config's name.
Never carries a credential. Assigning it replaces the label.
scheme
property
shell
property
A shell bound to this host: host.shell.run(...) is host.run(...).
host.shell.session(...) and with host.shell as session: open a
persistent session. Called with keywords --
host.shell(cwd="/srv/app", env={"TZ": "UTC"}) -- it returns a shell
carrying those defaults, applied to every later run and session
that does not pass its own value. env merges per key; cwd,
encoding, and errors override. A shell's attributes are read-only.
shell_flavour
property
The explicitly known shell language used by this host.
__enter__()
Source code in src/hostctl/host/_host.py
256 257 258 259 260 261 262 263 264 265 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/host/_host.py
267 268 269 270 271 272 273 274 | |
_run_selector()
The :class:~hostctl.provider.ProviderSelector backing :meth:run.
None for a host that does not select a command provider at all.
The default finds the attribute used by the provider-composed system
hosts; assemblies that name theirs differently override this.
Source code in src/hostctl/host/_host.py
206 207 208 209 210 211 212 213 | |
close()
Close transport resources; must be safe to call repeatedly.
Source code in src/hostctl/host/_host.py
253 254 | |
connect()
Open transport resources; local/default implementations are no-op.
Source code in src/hostctl/host/_host.py
250 251 | |
info()
Return normalized system information without inferred values.
Source code in src/hostctl/host/_host.py
245 246 247 248 | |
path(*segments, backend=None)
Return a pathlib-compatible path for this host.
Source code in src/hostctl/host/_host.py
276 277 278 279 280 | |
run(*cmds, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Run commands and return a subprocess-compatible result.
A string is verbatim shell text. A tuple/list is one quoted argv
command. :class:Exec is the only direct-execution spelling: it runs
one program with an argv and no shell layer, and it cannot be combined
with other commands. A path anywhere else is an ordinary value that
stringifies. Multiple top-level commands are joined by the selected
shell's command separator.
Two defaults differ from :func:subprocess.run deliberately, and
both are the opposite of it: check is True, so a non-zero
status raises :class:subprocess.CalledProcessError unless the
caller passes check=False; and capture_output is True, so
output is captured rather than inherited by a process that may have
no console. The result type and every other keyword do follow
subprocess.
timeout is seconds for this call, None for no limit; left out,
the config's timeout.total applies (see
:attr:HostConfig.timeouts).
Source code in src/hostctl/host/_host.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | |
spawn(*cmds, executable=None, cwd=None, env=None, terminal=None, encoding=None, errors=None)
Start a persistent process controlled through a synchronous facade.
Source code in src/hostctl/host/_host.py
282 283 284 285 286 287 288 289 290 291 292 293 294 295 | |
hostctl.HostConfig(*args, **secrets)
HostConfig()
HostConfig(connection_string: str, /, **secrets: str)
Bases: ABC
Secret-safe connection configuration and extensible URI dispatch.
Source code in src/hostctl/host/_config.py
87 88 89 90 91 | |
_lifecycle_lock = _threading.Lock()
class-attribute
instance-attribute
_opened_host = None
class-attribute
instance-attribute
_timeout_defaults = {}
class-attribute
_uri_entry_points = None
class-attribute
_uri_plugin_failures = {}
class-attribute
_uri_plugins_loaded = set()
class-attribute
_uri_registry_cache = None
class-attribute
_uri_registry_generation = 0
class-attribute
_uri_registry_lock = _threading.RLock()
class-attribute
_uri_schemes = ()
class-attribute
connection_uri
property
Credential-safe canonical connection URI.
name
property
writable
A short label for the target, for a log line: never a credential.
By default the authority of connection_uri without its userinfo
(nas.example, node:2222), else its path (vm1), else
localhost. Assigning it replaces the label.
scheme
property
timeouts
property
timeout with each DEFAULT replaced by what the transport applies.
A field the transport does not support stays DEFAULT; so does a
system config's total, which the selected provider decides. A
config without a timeout attribute reports Timeout().
uri_credentials = None
class-attribute
__enter__()
Source code in src/hostctl/host/_config.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/host/_config.py
241 242 243 244 245 246 247 248 249 250 251 | |
__init_subclass__(*, schemes=(), replace=False, **kwargs)
Source code in src/hostctl/host/_config.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
__str__()
Source code in src/hostctl/host/_config.py
200 201 | |
_claim_schemes(wanted, replace)
classmethod
Refuse a scheme another live config already claims.
A redefinition (same module and name: a reload, or a test that builds
the class per call) replaces its predecessor. replace=True takes the
scheme over from the previous owner.
Source code in src/hostctl/host/_config.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
_create_host()
Create the operational host represented by this configuration.
Documented override point: implement it in a subclass, and reach it
through :meth:open or with config as host.
Source code in src/hostctl/host/_config.py
209 210 211 212 213 214 215 216 | |
_default_name()
Source code in src/hostctl/host/_config.py
189 190 191 192 193 194 195 196 197 198 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Construct from a URI selected by :meth:_matches_uri.
Documented override point for a config subclass; callers use
HostConfig(uri).
Source code in src/hostctl/host/_config.py
363 364 365 366 367 368 369 370 371 372 | |
_from_uri(uri, **credentials)
classmethod
Dispatch a connection URI to a registered configuration.
A scheme://user:secret@host URI is accepted: the password is
extracted into the credential arguments and stripped from the parsed
authority, so it is never stored where connection_uri or repr()
would render it. Use :func:redact_uri before showing a URI that may
still carry one.
Explicit credentials override the URI's: password= replaces its
whole password field ("" meaning no password), and username=
replaces its user where the selected config accepts one.
The password field is parsed by :func:parse_credentials, so a newline
separates trailing key:value extras -- an OTP or other second factor
travels through the same field. The newline may be written raw: it is
percent-encoded before parsing (urlsplit would otherwise delete it),
and decoded again when the password is read. A control character in the
host is still refused, because no encoding makes that meaningful.
Source code in src/hostctl/host/_config.py
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
_lifecycle()
The instance's lifecycle lock, created on first use if needed.
Source code in src/hostctl/host/_config.py
93 94 95 96 97 98 99 100 101 102 | |
_matches_uri(parsed)
classmethod
Whether this implementation accepts a parsed URI.
Source code in src/hostctl/host/_config.py
358 359 360 361 | |
_refresh_uri_registry()
classmethod
Clear URI implementation discovery for tests and newly loaded plugins.
Source code in src/hostctl/host/_config.py
374 375 376 377 378 379 380 381 382 | |
_uri_credentials_for(parsed)
classmethod
The credential names this implementation accepts for parsed.
uri_credentials unless a scheme of the class narrows it.
Source code in src/hostctl/host/_config.py
326 327 328 329 330 331 332 333 334 | |
_uri_implementations(requested_scheme=None)
classmethod
Source code in src/hostctl/host/_config.py
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 | |
open()
Return a host context manager for this configuration.
Source code in src/hostctl/host/_config.py
218 219 220 | |
supported_credentials(uri)
classmethod
Credential names the implementation selected by uri accepts.
None means the implementation declares no whitelist and takes
whatever it is given. Use this for an ambient credential -- one
supplied for a session rather than for a single call, such as the
CLI's HOSTCTL_PASSWORD -- which should be offered only where it can
be used. An explicitly passed credential still fails closed, because
there a name the config does not know is a typo worth reporting.
Source code in src/hostctl/host/_config.py
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | |
hostctl.HostInfo(hostname=None, os_family=None, os_name=None, os_version=None, architecture=None)
dataclass
Normalized system information; unavailable values remain None.
architecture = None
class-attribute
instance-attribute
hostname = None
class-attribute
instance-attribute
os_family = None
class-attribute
instance-attribute
os_name = None
class-attribute
instance-attribute
os_version = None
class-attribute
instance-attribute
hostctl.Timeout(total=DEFAULT, *, connect=DEFAULT, read=DEFAULT, write=DEFAULT)
dataclass
How long a host's operations may take, field by field, in seconds.
total bounds a whole command and is the default of run(timeout=);
connect bounds establishing a connection, session or port; read one
reply or one read; write one write. Each field is DEFAULT (the
transport's own), None (no limit) or a positive number of seconds;
read and write may also be 0, "do not wait". Timeout(30) sets
total only. A transport that cannot honour a field, or its 0,
refuses it when the config is built.
Raises TypeError for a value that is not a number, None or DEFAULT,
and ValueError for a negative number, infinity, or zero as total or
connect.
Source code in src/hostctl/host/_timeout.py
80 81 82 83 84 85 86 87 88 89 | |
connect = DEFAULT
class-attribute
instance-attribute
read = DEFAULT
class-attribute
instance-attribute
total = DEFAULT
class-attribute
instance-attribute
write = DEFAULT
class-attribute
instance-attribute
__repr__()
Source code in src/hostctl/host/_timeout.py
112 113 114 115 116 117 118 | |
coerce(value)
classmethod
A Timeout for what a config's timeout= accepts.
A Timeout is returned as it is; a number or None sets total
only (coerce(60) == Timeout(total=60)); DEFAULT is
Timeout(). Anything else raises TypeError.
Source code in src/hostctl/host/_timeout.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
hostctl.DEFAULT = _Default.DEFAULT
module-attribute
hostctl.LocalConfig(*, timeout=Timeout())
Bases: HostConfig
This machine, addressed as local:.
It takes no host, no port and no credentials -- there is nothing to
connect to -- so its URI is a bare scheme, with ?timeout= when the
default of run(timeout=) is set.
Source code in src/hostctl/transports/local/_host.py
56 57 58 | |
_timeout_defaults = {'total': None}
class-attribute
instance-attribute
connection_uri
property
timeout = timeout
class-attribute
instance-attribute
uri_credentials = ()
class-attribute
instance-attribute
__repr__()
Source code in src/hostctl/transports/local/_host.py
60 61 62 63 | |
_create_host()
Source code in src/hostctl/transports/local/_host.py
79 80 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/local/_host.py
70 71 72 73 74 75 76 77 | |
hostctl.LocalHost(config=None, *, executor_providers=(), path_providers=(), **options)
Bases: SystemHost
This machine, assembled from the local providers.
A SystemHost like every other, not a second implementation of one:
provider selection, failover, capabilities and the direct/shell
dispatch are SystemHost's, so local and remote hosts cannot diverge.
What is genuinely local stays here: the system family and shell come
from os.name rather than a config, info() reports this process's
own platform, and the default providers are the local ones.
Source code in src/hostctl/transports/local/_host.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
config_type = LocalConfig
class-attribute
instance-attribute
default_shell = POWERSHELL
instance-attribute
shell_flavour
property
The local shell, or a refusal naming the OS it could not place.
system_family = 'windows'
instance-attribute
info()
This process's own platform, with no transport to ask.
Source code in src/hostctl/transports/local/_host.py
164 165 166 167 168 169 170 171 172 | |
path(*segments, backend=None)
A plain pathapi path, not a composite.
The composite exists to select between providers per operation and to pin a route; a local host reaches one filesystem through one provider, so it would add a layer with nothing to decide. Callers also rely on a local path being an ordinary pathlib object.
With no segments this returns the empty relative path, as every
other transport does, not os.getcwd(); it still resolves against
this process's working directory.
Source code in src/hostctl/transports/local/_host.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
hostctl.SSHConfig(host, port=None, username=None, default_username=None, password=None, client_keys=None, executable=None, verify=True, known_hosts=(), shell=POSIX_SHELL, pathname=PosixPathname, login_shell='auto', timeout=_TimeoutField(), ssh_config=USER_SSH_CONFIG, connect_options=dict())
dataclass
Bases: HostConfig
Explicit SSH transport, authentication, and target-shell settings.
_timeout_defaults = {'total': None, 'connect': 30.0}
class-attribute
instance-attribute
client_keys = dataclasses.field(default=None, repr=False)
class-attribute
instance-attribute
connect_options = dataclasses.field(default_factory=dict, repr=False)
class-attribute
instance-attribute
connection_uri
property
default_username = None
class-attribute
instance-attribute
executable = None
class-attribute
instance-attribute
host
instance-attribute
known_hosts = ()
class-attribute
instance-attribute
login_shell = 'auto'
class-attribute
instance-attribute
password = dataclasses.field(default=None, repr=False)
class-attribute
instance-attribute
pathname = PosixPathname
class-attribute
instance-attribute
port = None
class-attribute
instance-attribute
resolved_login_shell
property
login_shell with "auto" answered from the shell.
With pathname="auto" the answer needs the server: use the
transport's login_shell.
shell = POSIX_SHELL
class-attribute
instance-attribute
ssh_config = USER_SSH_CONFIG
class-attribute
instance-attribute
timeout = _TimeoutField()
class-attribute
instance-attribute
uri_credentials = ('username', 'password', 'client_keys', 'known_hosts')
class-attribute
instance-attribute
username = None
class-attribute
instance-attribute
verify = True
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/transports/ssh/_config.py
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 | |
_create_host()
Source code in src/hostctl/transports/ssh/_config.py
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 | |
_create_transport()
Create the private SSH service shared by executor/path providers.
Source code in src/hostctl/transports/ssh/_config.py
410 411 412 413 414 | |
_default_name()
Source code in src/hostctl/transports/ssh/_config.py
324 325 326 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/ssh/_config.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 | |
connect_opts()
Source code in src/hostctl/transports/ssh/_config.py
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | |
hostctl.WinRMConfig(host, username, password=None, auth='ntlm', port=None, tls=False, verify=True, message_encryption='auto', timeout=_TimeoutField(), provider='auto', cleartext_auth=False)
dataclass
Bases: HostConfig
Connection and transport settings for the Windows provider.
_timeout_defaults = {'total': 30.0, 'connect': 30.0}
class-attribute
instance-attribute
auth = 'ntlm'
class-attribute
instance-attribute
cleartext_auth = False
class-attribute
instance-attribute
connection_uri
property
endpoint
property
host
instance-attribute
message_encryption = 'auto'
class-attribute
instance-attribute
password = dataclasses.field(default=None, repr=False)
class-attribute
instance-attribute
port = None
class-attribute
instance-attribute
provider = 'auto'
class-attribute
instance-attribute
scheme
property
timeout = _TimeoutField()
class-attribute
instance-attribute
tls = False
class-attribute
instance-attribute
uri_credentials = ('username', 'password')
class-attribute
instance-attribute
username
instance-attribute
verify = True
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/transports/winrm/_config.py
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
_create_host()
Source code in src/hostctl/transports/winrm/_config.py
256 257 258 259 260 261 262 263 264 265 266 267 268 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/winrm/_config.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | |
hostctl.ContainerConfig(container, engine_url=None, verify=True, username=None, workdir=None, executable=None, shell='auto', pathname='auto', timeout=_TimeoutField(), client_factory=None, client_cert=None)
dataclass
Bases: HostConfig
Docker Engine target and in-container execution defaults.
_timeout_defaults = {'connect': 60.0}
class-attribute
instance-attribute
client_cert = dataclasses.field(default=None, repr=False)
class-attribute
instance-attribute
client_factory = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
connection_uri
property
container
instance-attribute
engine_url = None
class-attribute
instance-attribute
executable = None
class-attribute
instance-attribute
pathname = 'auto'
class-attribute
instance-attribute
shell = 'auto'
class-attribute
instance-attribute
timeout = _TimeoutField()
class-attribute
instance-attribute
uri_credentials = ('client_factory', 'client_cert')
class-attribute
instance-attribute
username = None
class-attribute
instance-attribute
verify = True
class-attribute
instance-attribute
workdir = None
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/transports/container/_config.py
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
_create_host()
Source code in src/hostctl/transports/container/_config.py
198 199 200 201 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/container/_config.py
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
hostctl.ContainerHost(config, *, executor_providers=(), path_providers=())
Bases: Host
A running container reached through the Docker Engine API.
Commands and paths are assembled from ordered providers
(:class:ContainerExecutorProvider and
:class:ContainerArchivePathProvider) without changing the public API.
The archive provider declares only the operations the Docker archive API
can perform, so an unsupported mutation is rejected outright instead of
falling through to another provider.
Source code in src/hostctl/transports/container/_host.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
_attrs = None
instance-attribute
_client = None
instance-attribute
_container = None
instance-attribute
_executor = ContainerExecutor(lambda: self.container, user=config.username, workdir=config.workdir)
instance-attribute
_executor_provider_selector = ProviderSelector(tuple(executor_providers) or (ContainerExecutorProvider(self._executor, connect=lambda: self.container, close=self._close_client),))
instance-attribute
_path_backend = None
instance-attribute
_path_provider_selector = ProviderSelector(tuple(path_providers) or (ContainerArchivePathProvider(self._archive_path),))
instance-attribute
capabilities
property
client
property
config = config
instance-attribute
container
property
executor
property
executor_providers
property
The ordered command providers backing :meth:run.
inspected_os
property
path_providers
property
The ordered filesystem providers backing :meth:path.
shell_flavour
property
_archive_path(*segments)
Build a Docker-archive-backed path with the configured flavour.
Source code in src/hostctl/transports/container/_host.py
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 | |
_close_client()
Source code in src/hostctl/transports/container/_host.py
283 284 285 286 287 288 289 290 291 292 293 294 295 | |
_run_selector()
Source code in src/hostctl/transports/container/_host.py
176 177 | |
_run_with_provider(provider, cmds, *, bufsize, executable, stdin, stdout, stderr, cwd, env, capture_output, check, encoding, errors, input, timeout, text)
Source code in src/hostctl/transports/container/_host.py
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 | |
close()
Source code in src/hostctl/transports/container/_host.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | |
connect()
Source code in src/hostctl/transports/container/_host.py
280 281 | |
info()
Source code in src/hostctl/transports/container/_host.py
321 322 323 324 325 326 327 328 329 330 331 332 | |
path(*segments, backend=None)
Source code in src/hostctl/transports/container/_host.py
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
run(*cmds, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Source code in src/hostctl/transports/container/_host.py
377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 | |
spawn(*cmds, executable=None, cwd=None, env=None, terminal=None, encoding=None, errors=None)
Start a persistent Docker exec process with an optional TTY.
Source code in src/hostctl/transports/container/_host.py
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 | |
hostctl.QEMUConfig(domain, transport='libvirt', connection=None, socket_path=None, ssh=None, timeout=_TimeoutField(), max_reply_size=48 * 1024 * 1024, shell='auto', pathname='auto', transport_factory=None, serial_console=None, path_helper=None)
dataclass
Bases: HostConfig
Guest identity, QGA transport, and guest semantic selections.
_COMMON_CREDENTIALS = ('transport_factory', 'serial_console', 'path_helper')
class-attribute
instance-attribute
_opens_agent_stream
property
Whether hostctl connects the agent stream (unix, ssh) itself.
_timeout_defaults = {'total': None, 'connect': DEFAULT, 'read': 10.0}
class-attribute
instance-attribute
connection = None
class-attribute
instance-attribute
connection_uri
property
domain
instance-attribute
max_reply_size = 48 * 1024 * 1024
class-attribute
instance-attribute
path_helper = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
pathname = 'auto'
class-attribute
instance-attribute
serial_console = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
shell = 'auto'
class-attribute
instance-attribute
socket_path = None
class-attribute
instance-attribute
ssh = dataclasses.field(default=None, repr=False)
class-attribute
instance-attribute
timeout = _TimeoutField()
class-attribute
instance-attribute
timeouts
property
transport = 'libvirt'
class-attribute
instance-attribute
transport_factory = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
uri_credentials = ('username', 'password', 'client_keys', 'known_hosts', 'transport_factory', 'serial_console', 'path_helper')
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/transports/qemu/_config.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
_create_host()
Source code in src/hostctl/transports/qemu/_config.py
271 272 273 274 | |
_default_name()
Source code in src/hostctl/transports/qemu/_config.py
139 140 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/qemu/_config.py
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
_uri_credentials_for(parsed)
classmethod
Source code in src/hostctl/transports/qemu/_config.py
182 183 184 185 186 187 188 | |
hostctl.QEMUHost(config)
Bases: Host
A guest OS reached through QEMU Guest Agent.
Source code in src/hostctl/transports/qemu/_host.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | |
_POSIX_ONLY_COMMANDS = frozenset({'guest-get-cpustats', 'guest-get-diskstats'})
class-attribute
instance-attribute
_WINDOWS_ONLY_COMMANDS = frozenset({'guest-get-devices'})
class-attribute
instance-attribute
_agent_version = None
instance-attribute
_commands = None
instance-attribute
_executor = QEMUExecutor(lambda: self.transport, agent_timeout=config.timeouts.read, merged_capture=self._merges_output)
instance-attribute
_hostname = None
instance-attribute
_lock = threading.RLock()
instance-attribute
_os_info = {}
instance-attribute
_path_backend = None
instance-attribute
_path_helper = _UNPROBED
instance-attribute
_path_provider = None
instance-attribute
_ssh_transport = None
instance-attribute
_transport = None
instance-attribute
_windows
property
capabilities
property
config = config
instance-attribute
executor
property
path_provider
property
The QGA path provider, once :meth:path has assembled it.
shell_flavour
property
supported_commands
property
transport
property
_build_transport()
Source code in src/hostctl/transports/qemu/_host.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 | |
_close_locked()
Source code in src/hostctl/transports/qemu/_host.py
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | |
_connect_timeout()
The agent-stream connect bound; omitted, it shares the first reply's.
Source code in src/hostctl/transports/qemu/_host.py
133 134 135 136 | |
_discover()
Source code in src/hostctl/transports/qemu/_host.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | |
_guest_path_helper()
Probe the guest for a metadata/namespace helper, or answer None.
QGA's file RPCs move bytes and nothing else, so stat, scandir
and every mutation need a guest-side helper. One is built here from
guest-exec -- positively probed, never assumed: stat -c and
find -printf are GNU spellings busybox and the BSDs do not share,
and a helper that assumes GNU and silently mis-parses is worse than
none. A failed probe returns None, which leaves the backend in
exactly the degraded mode it had before: reads and writes keep
working, and only the operations that genuinely need a helper are
unavailable.
QEMUConfig(path_helper=...) overrides this entirely.
Source code in src/hostctl/transports/qemu/_host.py
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | |
_merges_output()
Whether the agent can interleave stderr into stdout itself.
capture-output: "merged" exists from qemu-ga 8.0 and is compiled
out of the Windows agent, so both facts must be positively known.
Source code in src/hostctl/transports/qemu/_host.py
284 285 286 287 288 289 290 291 292 293 294 295 | |
_probe_os_family()
The guest's OS family, or None when nothing positively said.
Every branch here is positive evidence. Reading "not Windows" out of
an guest-get-osinfo that never ran is a guess, and the project's
rule for auto-detection is to raise rather than guess.
Source code in src/hostctl/transports/qemu/_host.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
_qga_path(*segments)
Build a QGA-backed guest path with the configured pathname flavour.
Source code in src/hostctl/transports/qemu/_host.py
407 408 409 410 411 412 413 414 415 416 417 418 | |
close()
Source code in src/hostctl/transports/qemu/_host.py
220 221 222 | |
connect()
Source code in src/hostctl/transports/qemu/_host.py
172 173 174 175 176 | |
info()
Source code in src/hostctl/transports/qemu/_host.py
315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |
open_serial(*, encoding=None, errors=None)
Open the explicitly configured VM console; str I/O with encoding.
Source code in src/hostctl/transports/qemu/_host.py
161 162 163 164 165 166 167 168 169 170 | |
path(*segments, backend=None)
Source code in src/hostctl/transports/qemu/_host.py
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
run(*cmds, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Source code in src/hostctl/transports/qemu/_host.py
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 | |
hostctl.SerialConfig(device, baudrate=115200, bytesize=8, parity='N', stopbits=1, xonxoff=False, rtscts=False, dsrdtr=False, timeout=_TimeoutField(), inter_byte_timeout=None, exclusive=None, profile=RawConsoleProfile(), serial_factory=None, serial_port=None)
dataclass
Bases: HostConfig
Serial transport settings; credentials and profiles stay out of URIs.
_timeout_defaults = {'total': None, 'read': 0.1, 'write': 10.0}
class-attribute
instance-attribute
_timeout_zero = ('read', 'write')
class-attribute
instance-attribute
baudrate = 115200
class-attribute
instance-attribute
bytesize = 8
class-attribute
instance-attribute
connection_uri
property
device
instance-attribute
dsrdtr = False
class-attribute
instance-attribute
exclusive = None
class-attribute
instance-attribute
inter_byte_timeout = None
class-attribute
instance-attribute
parity = 'N'
class-attribute
instance-attribute
profile = dataclasses.field(default_factory=RawConsoleProfile, repr=False, compare=False)
class-attribute
instance-attribute
rtscts = False
class-attribute
instance-attribute
serial_factory = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
serial_port = dataclasses.field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
settings = None
class-attribute
stopbits = 1
class-attribute
instance-attribute
timeout = _TimeoutField()
class-attribute
instance-attribute
uri_credentials = ('profile', 'serial_factory', 'serial_port')
class-attribute
instance-attribute
xonxoff = False
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/transports/serial/_host.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
_create_host()
Source code in src/hostctl/transports/serial/_host.py
278 279 | |
_default_name()
Source code in src/hostctl/transports/serial/_host.py
196 197 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/transports/serial/_host.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | |
hostctl.SerialHost(config)
Bases: Host
Host facade over one exclusive serial console byte stream.
Source code in src/hostctl/transports/serial/_host.py
285 286 287 288 289 290 291 292 293 294 295 296 297 298 | |
_executor = SerialExecutor(typing.cast(SerialSettings, config.settings), serial_factory=config.serial_factory, serial_port=config.serial_port, owns_serial_port=False if config.serial_port is not None else True)
instance-attribute
_negotiated_on = None
instance-attribute
capabilities
property
config = config
instance-attribute
executor
property
shell
property
shell_flavour
property
The flavour the console profile declares (PosixConsoleProfile's shell).
close()
Source code in src/hostctl/transports/serial/_host.py
349 350 351 | |
connect()
Source code in src/hostctl/transports/serial/_host.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 | |
info()
Source code in src/hostctl/transports/serial/_host.py
353 354 | |
path(*segments, backend=None)
Source code in src/hostctl/transports/serial/_host.py
356 357 | |
run(*cmds, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Source code in src/hostctl/transports/serial/_host.py
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 | |
spawn(*cmds, executable=None, cwd=None, env=None, terminal=None, encoding=None, errors=None)
Source code in src/hostctl/transports/serial/_host.py
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | |
hostctl.SystemConfig(authority='localhost', *, shell=None, executor=None, path=None, provider_options=None, initializer=None, timeout=Timeout())
Bases: HostConfig
Base configuration for a logical system with one or more providers.
Abstract: concrete system families are :class:PosixConfig,
:class:WindowsConfig, and :class:IOSConfig. Each binds host_type
and uri_scheme; this class binds neither and cannot be instantiated
into a host on its own.
Source code in src/hostctl/system/_config.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
_timeout_defaults = {'total': DEFAULT}
class-attribute
instance-attribute
authority = authority or 'localhost'
instance-attribute
connection_uri
property
executors = None if executor is None else tuple(executor)
instance-attribute
host_type = None
class-attribute
options = {key: value for key, value in (('provider_options', provider_options), ('initializer', initializer)) if value is not None}
instance-attribute
paths = None if path is None else tuple(path)
instance-attribute
shell = shell
instance-attribute
timeout = timeout
class-attribute
instance-attribute
uri_scheme = ''
class-attribute
__repr__()
Source code in src/hostctl/system/_config.py
88 89 90 91 92 93 94 95 96 97 98 99 | |
_create_host()
Source code in src/hostctl/system/_config.py
165 166 167 168 169 170 171 172 173 174 175 176 177 | |
_default_name()
Source code in src/hostctl/system/_config.py
101 102 | |
_from_parsed_uri(parsed, **credentials)
classmethod
Source code in src/hostctl/system/_config.py
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | |
hostctl.SystemHost(config=None, *, executor_providers=None, path_providers=None, shell=None, info=None, initializer=None)
Bases: Host
Host orchestration shared by POSIX, Windows, and IOS systems.
Source code in src/hostctl/system/_host.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
_closed_targets = set()
instance-attribute
_connected = False
instance-attribute
_connected_providers = []
instance-attribute
_executor_selector = self._selector(executor_providers, 'executor')
instance-attribute
_fallbacks_lock = threading.Lock()
instance-attribute
_fallbacks_logged = set()
instance-attribute
_info = info
instance-attribute
_initializer = initializer if initializer is not None else config_options.get('initializer') if isinstance(config_options, dict) else None
instance-attribute
_initializer_generation = False
instance-attribute
_initializing = False
instance-attribute
_lifecycle_lock = threading.RLock()
instance-attribute
_path_selector = self._selector(path_providers, 'path')
instance-attribute
_shell = None if self._shell_resolver is not None else shell_flavour(shell) if shell is not None else shell_flavour(getattr(self.config, 'shell', None)) if getattr(self.config, 'shell', None) else self.default_shell
instance-attribute
_shell_resolver = shell if callable(shell) and not isinstance(shell, type) else None
instance-attribute
capabilities
property
config = config if config is not None else self.config_type()
instance-attribute
config_type
class-attribute
default_shell = None
class-attribute
instance-attribute
executor
property
The selected provider, presented as an Executor.
A bare ExecutorProvider has no executor_capabilities, so a
Shell built on one inferred CWD/ENV support from the callable's
signature instead of reading the provider's declared set -- and
inferred it wrongly. The wrapper carries the declared capabilities
and dispatches through run(), so the host's own rendering and
failover still apply.
executor_capabilities
property
executor_providers
property
The ordered command providers backing :meth:run.
path_providers
property
The ordered filesystem providers backing :meth:path.
provider_details
property
Every provider's availability, executors first; dispatches nothing.
provider_resolvers = {}
class-attribute
shell_flavour
property
system_family = 'generic'
class-attribute
instance-attribute
_close_locked()
Source code in src/hostctl/system/_host.py
521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 | |
_collect_info()
info() with what it learned about the providers.
Returns the HostInfo, whether some provider answered, and the
transport errors raised by those that did not. info() stays best
effort; a caller that must tell "nothing answered" from an answer
(the command line's exit status) reads the rest.
Source code in src/hostctl/system/_host.py
562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 | |
_composite_path_type()
The path grammar this system family actually has.
Any other family raises: an IOSHost is session/command-only until
an IOS path grammar is designed, and POSIX rules would misread
flash:/config.text, where : is a device separator rather than part
of a name.
Source code in src/hostctl/system/_host.py
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 | |
_connect_locked()
Source code in src/hostctl/system/_host.py
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 | |
_ensure_provider_connected(provider)
Source code in src/hostctl/system/_host.py
966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 | |
_execute_command_line(provider, flavour, cmds, *, executable, context, options)
Render one finalized command line as (command, args, options).
The one place a ShellCommand is turned into a dispatch: it carries
an out-of-band environment for providers that cannot embed one, and
it is the only path that needs the login-shell layer.
Source code in src/hostctl/system/_host.py
778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 | |
_log_fallback(kind, provider, skipped)
One WARNING per host and skipped provider: the operation was served by a provider ranked below one that is unavailable.
Source code in src/hostctl/system/_host.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
_note_provider_in_use(provider)
Mark a provider's transport as live again, without connecting it.
close() skips a transport it has already closed; a transport that
reconnects lazily behind an operation would otherwise never be closed
again.
Source code in src/hostctl/system/_host.py
955 956 957 958 959 960 961 962 963 964 | |
_out_of_band_env(provider, rendered, options)
staticmethod
Carry a flavour's out-of-band environment, or refuse to lose it.
ShellCommand.environment means "the environment is NOT embedded in
this command; send it separately" -- which is what _SSHTransport.run
does. SystemHost passed only .command, so a registered flavour
for a shell that cannot export variables lost every one of them in
silence. Latent for the four built-in flavours, which all return
None; that is a reason to fix it cheaply, not to keep it.
Source code in src/hostctl/system/_host.py
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 | |
_provider_probe(selector, provider)
staticmethod
Source code in src/hostctl/system/_host.py
347 348 349 | |
_render_dispatch(provider, cmds, kwargs)
The (command, args, options) this call sends to provider.
Pure: it connects nothing and sends nothing, so it can be run again with a masked environment.
Source code in src/hostctl/system/_host.py
823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 | |
_run_initializer_locked(connected)
Run the session initializer once per connection generation.
Called from the explicit connect() and from the lazy path, because
run(), path() and spawn() all open a generation without going
through connect() -- and providers.md promises the initializer runs
once per generation, before the host is handed to the caller. It used
to run only from connect(), so a host used lazily was bootstrapped
silently not at all.
The _initializing guard is what makes the lazy path safe: the
initializer is handed this host and legitimately calls run(), which
re-enters here through _ensure_provider_connected (the lock is an
RLock for exactly that), and _initializer_generation is only set
afterwards -- so without the guard the nested call would start the
initializer again.
Source code in src/hostctl/system/_host.py
472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 | |
_run_with_provider(provider, cmds, **kwargs)
Source code in src/hostctl/system/_host.py
806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 | |
_select_capable(capability, operation)
Select an executor provider offering capability.
An unsupported operation is NotImplementedError, the documented
contract run() and path() also follow; select() alone would
report it as OperationNotStartedError.
Source code in src/hostctl/system/_host.py
984 985 986 987 988 989 990 991 992 993 994 995 996 | |
_selector(providers, kind)
A selector over providers that reports a fallback to this host.
Source code in src/hostctl/system/_host.py
169 170 171 172 173 174 175 176 177 178 179 180 | |
_shell_context(provider, cwd, env)
staticmethod
Which of cwd/env the shell must embed for this provider.
Source code in src/hostctl/system/_host.py
769 770 771 772 773 774 775 776 | |
close()
Source code in src/hostctl/system/_host.py
517 518 519 | |
connect()
Source code in src/hostctl/system/_host.py
418 419 420 | |
default_providers()
The (executor, path) descriptor names used when the config names none.
Override per host class; it may read self.config. A default
ssh/sftp is dropped when the ssh extra is missing.
Source code in src/hostctl/system/_host.py
206 207 208 209 210 211 212 213 214 | |
info()
Source code in src/hostctl/system/_host.py
559 560 | |
path(*segments, backend=None)
Source code in src/hostctl/system/_host.py
659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 | |
replace_providers(*, executor=None, path=None)
Swap either provider set; None keeps it.
Starts a new connection generation: the session initializer runs again before the next operation. A replaced provider's transport is closed unless a provider that stays shares it, and a transport shared by several replaced providers is closed once. A path obtained earlier still routes to the old providers, so obtain it again. An invalid set (duplicate names) raises before anything changes.
Source code in src/hostctl/system/_host.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
resolve_providers(executor=None, path=None)
Build providers from descriptor names, in order.
None takes the config's executors/paths, or default_providers()
when the config names none. Each resolver runs once per call, so the
two legs of one transport share it: replace both sets with one call's
result. An unknown name raises ValueError listing the known ones.
Source code in src/hostctl/system/_host.py
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | |
run(*cmds, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Source code in src/hostctl/system/_host.py
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 | |
runspace()
Return a provider-owned typed runspace when one is available.
Source code in src/hostctl/system/_host.py
1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 | |
spawn(*cmds, **options)
Source code in src/hostctl/system/_host.py
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 | |
hostctl.PosixConfig(authority='localhost', *, shell=None, executor=None, path=None, provider_options=None, initializer=None, timeout=Timeout())
Bases: SystemConfig
Connection identity for a :class:PosixHost (posix:, posix+ssh:).
Source code in src/hostctl/system/_config.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
host_type = PosixHost
class-attribute
instance-attribute
uri_scheme = 'posix'
class-attribute
instance-attribute
hostctl.PosixHost(config=None, *, executor_providers=None, path_providers=None, shell=None, info=None, initializer=None)
Bases: SystemHost
A POSIX system: POSIX-shell command construction and POSIX paths.
Compose it from providers directly, or open an :class:SSHConfig.
Source code in src/hostctl/system/_host.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
default_shell = POSIX_SHELL
class-attribute
instance-attribute
system_family = 'posix'
class-attribute
instance-attribute
hostctl.WindowsConfig(authority='localhost', *, shell=None, executor=None, path=None, provider_options=None, initializer=None, timeout=Timeout())
Bases: SystemConfig
Connection identity for a :class:WindowsHost (windows:,
windows+winrm:).
Source code in src/hostctl/system/_config.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
host_type = WindowsHost
class-attribute
instance-attribute
uri_scheme = 'windows'
class-attribute
instance-attribute
hostctl.WindowsHost(config=None, *, executor_providers=None, path_providers=None, shell=None, info=None, initializer=None)
Bases: SystemHost
A Windows system: PowerShell command construction and Windows paths.
Compose it from providers directly, or open a :class:WinRMConfig.
Source code in src/hostctl/system/_host.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
default_shell = POWERSHELL
class-attribute
instance-attribute
system_family = 'windows'
class-attribute
instance-attribute
hostctl.IOSConfig(authority='localhost', *, shell=None, executor=None, path=None, provider_options=None, initializer=None, timeout=Timeout())
Bases: SystemConfig
Connection identity for an :class:IOSHost (ios:), which is
session- and command-only.
Source code in src/hostctl/system/_config.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
host_type = IOSHost
class-attribute
instance-attribute
uri_scheme = 'ios'
class-attribute
instance-attribute
hostctl.IOSHost(config=None, *, executor_providers=None, path_providers=None, shell=None, info=None, initializer=None)
Bases: SystemHost
A Cisco IOS-style device: session and command only, by design.
It configures no shell flavour and no path grammar, which is a
deliberate gap rather than an unfinished one: IOS command syntax is
not a shell, and quoting it as one would corrupt ordinary commands.
So commands must be :class:Exec (or raw text the device
understands), and :meth:path raises NotImplementedError until
an IOS path grammar exists.
Source code in src/hostctl/system/_host.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
default_shell = None
class-attribute
instance-attribute
system_family = 'ios'
class-attribute
instance-attribute
Executors and processes
hostctl.Exec(program, *args)
dataclass
One command executed directly, with no shell layer.
Exec(program, *args) names a program and its argv. The program may be
an absolute path or a bare name the target resolves through PATH; a
bare name has no other spelling, since a plain string is always shell text.
This is the explicit marker for direct execution. A path used anywhere else is an ordinary value that stringifies, so several commands can be written without one of them silently becoming an executable::
host.run(Exec("/bin/ls", "-l")) # one direct command
host.run(Exec("ls", "-l")) # PATH-resolved, no shell
host.run(Exec("/bin/a"), Exec("/bin/b")) # two direct commands
host.run(PurePosixPath("/bin/a"), PurePosixPath("/bin/b"))
# two ordinary shell commands
Arguments are argv values, never nested commands: a list or tuple raises
TypeError rather than blurring argv and shell semantics.
A deliberately non-iterable container. ShellFlavour.command_text
dispatches structured commands on Iterable, so an iterable marker
would be quoted into an argv string instead of taking the direct branch.
Source code in src/hostctl/shell/_grammar.py
57 58 59 60 61 62 63 64 65 66 67 68 | |
args
instance-attribute
program
instance-attribute
hostctl.Executor
Bases: Protocol[_Result]
Callable executor receiving one command string and execution options.
executor_capabilities
instance-attribute
__call__(command, *args, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=None, check=None, encoding=None, errors=None, input=None, timeout=None, text=None, **options)
Source code in src/hostctl/executor.py
524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 | |
hostctl.ExecutionOptions
Bases: TypedDict
Shell-agnostic subprocess-style options understood by executors.
capture_output
instance-attribute
check
instance-attribute
cwd
instance-attribute
encoding
instance-attribute
env
instance-attribute
errors
instance-attribute
input
instance-attribute
stderr
instance-attribute
stdin
instance-attribute
stdout
instance-attribute
text
instance-attribute
timeout
instance-attribute
hostctl.LocalExecutor
Bases: Executor[CompletedProcess]
Execute direct argv or finalized shell invocations locally.
Not a BufferedExecutor: subprocess is the contract that base
reproduces, and it hands the child real descriptors rather than buffers.
executor_capabilities = frozenset((ExecutorCapability.ARGS, ExecutorCapability.CWD, ExecutorCapability.ENV))
class-attribute
instance-attribute
__call__(command, *args, bufsize=-1, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=None, text=None, **options)
Source code in src/hostctl/transports/local/_executor.py
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
The bases a transport written outside hostctl subclasses; the override points and a worked example are in Writing an executor.
hostctl.executor.BufferedExecutor
Bases: Executor[CompletedProcess]
Base of an executor whose transport returns a command's whole output.
A subclass declares executor_capabilities and implements _exchange.
The base implements the subprocess-shaped __call__ every hostctl
executor shares: unknown options and undeclared args/cwd/env are
refused, input=/stdin= become bytes, the output type follows
wants_text, capture and stream targets follow capture_streams and
dispatch_output, a non-zero status raises under check=True (the
default), and a timeout raises expired()'s TimeoutExpired.
executor_capabilities = frozenset()
class-attribute
instance-attribute
input_unsupported = None
class-attribute
instance-attribute
timeout_unsupported = None
class-attribute
instance-attribute
__call__(command, *args, bufsize=-1, stdin=None, stdout=None, stderr=None, cwd=None, env=None, capture_output=True, check=True, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None, **options)
Source code in src/hostctl/executor.py
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 | |
_default_timeout()
The limit of a call made without timeout=; None (no limit) here.
Source code in src/hostctl/executor.py
826 827 828 | |
_dispatch_request(request, call)
Source code in src/hostctl/executor.py
848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 | |
_exchange(request)
Run request and return (stdout, stderr, returncode); override it.
Either stream may be bytes, str or None (empty). Raise
subprocess.TimeoutExpired when request.timeout passes (build it
with expired() to set orphaned, pid and partial output), and
OperationNotStartedError only when nothing was sent.
Source code in src/hostctl/executor.py
814 815 816 817 818 819 820 821 822 823 824 | |
_normalize_error(exc)
Map a transport error to the one to raise; exc itself by default.
A subprocess.TimeoutExpired returned here is raised in expired()'s
shape; anything else is raised chained to exc.
Source code in src/hostctl/executor.py
830 831 832 833 834 835 836 | |
_raise_normalized(exc, normalized, request, call)
staticmethod
Source code in src/hostctl/executor.py
838 839 840 841 842 843 844 845 846 | |
hostctl.executor.StreamingExecutor
Bases: BufferedExecutor
Base of an executor whose transport delivers output as it arrives.
A subclass implements _send and _receive, and _abandon when it can
stop a running command. The base sends, then receives until a status
arrives: captured output is collected, and output routed elsewhere (a
file, None for the console) is written as each piece arrives. When the
deadline passes it calls _abandon and raises TimeoutExpired carrying
the output captured so far. OperationNotStartedError declines the
provider only from _send; out of _receive, after the command was
sent, it is raised as ConnectionError so the command is not replayed.
_abandon(request)
Stop the command after a timeout or a failure; True if it may still run.
The default stops nothing, so the command is reported orphaned.
Source code in src/hostctl/executor.py
936 937 938 939 940 941 | |
_dispatch_request(request, call)
Source code in src/hostctl/executor.py
949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 | |
_receive(request, wait)
Return the next (stdout, stderr, returncode) output; override it.
Wait at most wait seconds (None: until something arrives) and
return (b"", b"", None) when nothing did. A status other than
None ends the command; output returned with it is its last.
Source code in src/hostctl/executor.py
925 926 927 928 929 930 931 932 933 934 | |
_send(request)
Send request (command and input) to the target; override it.
Raise OperationNotStartedError only when no byte of it was sent.
Source code in src/hostctl/executor.py
918 919 920 921 922 923 | |
_stop(request)
Source code in src/hostctl/executor.py
943 944 945 946 947 | |
hostctl.executor.ExecutorRequest(command, args=(), input=None, merge_stderr=False, cwd=None, env=None, timeout=None, deadline=None)
dataclass
One command as a transport primitive of BufferedExecutor receives it.
command is the command text and args the native arguments as text,
empty unless the executor declares args. input is the whole standard
input, or None when the call sent none: the command then gets an empty
stdin (EOF), never a terminal. merge_stderr is stderr=STDOUT: merge
natively where the transport can, else return stderr apart and the base
appends it. cwd and env are None unless declared and passed; env
adds to the target's environment. timeout is the call's limit in seconds
(None: no limit) and deadline the same limit on time.monotonic().
args = ()
class-attribute
instance-attribute
command
instance-attribute
cwd = None
class-attribute
instance-attribute
deadline = None
class-attribute
instance-attribute
env = None
class-attribute
instance-attribute
input = None
class-attribute
instance-attribute
merge_stderr = False
class-attribute
instance-attribute
timeout = None
class-attribute
instance-attribute
remaining()
Seconds left before deadline, zero or less once it passed; None without one.
Source code in src/hostctl/executor.py
628 629 630 631 632 | |
hostctl.Process
Bases: Protocol
Synchronous control surface for a persistent child process.
Every member raises rather than carrying the conventional ... body.
Protocol members are NOT @abstractmethod, so five concrete adapters
inherit this class explicitly and an inherited-but-unimplemented member
was a real method returning None: instantiation succeeded,
isinstance(x, Process) passed, and process.wait(60) == 0 failed as
assert None == 0 -- or, in if process.wait():, read as "exited 0".
The conformance battery exists to catch exactly "advertised but not
implemented", and this base class was converting that mistake into a
silent wrong value.
Structural typing is unaffected: a class that satisfies the protocol without inheriting it never reaches these bodies.
returncode
property
__enter__()
Source code in src/hostctl/process.py
136 137 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/process.py
139 140 141 142 143 144 145 | |
close()
Source code in src/hostctl/process.py
133 134 | |
kill()
Source code in src/hostctl/process.py
130 131 | |
read(size=-1)
Source code in src/hostctl/process.py
106 107 | |
read_stderr(size=-1)
Source code in src/hostctl/process.py
109 110 | |
resize(columns, rows, pixel_width=0, pixel_height=0)
Source code in src/hostctl/process.py
115 116 117 118 119 120 121 122 | |
send_eof()
Source code in src/hostctl/process.py
112 113 | |
terminate()
Source code in src/hostctl/process.py
127 128 | |
wait(timeout=None)
Source code in src/hostctl/process.py
124 125 | |
write(data)
Source code in src/hostctl/process.py
103 104 | |
hostctl.TerminalOptions(term_type='xterm-256color', columns=80, rows=24, pixel_width=0, pixel_height=0)
dataclass
Requested pseudo-terminal type and initial dimensions.
columns = 80
class-attribute
instance-attribute
pixel_height = 0
class-attribute
instance-attribute
pixel_width = 0
class-attribute
instance-attribute
rows = 24
class-attribute
instance-attribute
size
property
term_type = 'xterm-256color'
class-attribute
instance-attribute
__post_init__()
Source code in src/hostctl/process.py
42 43 44 45 46 47 48 | |
Shells and sessions
hostctl.ShellFlavour
Bases: ABC
Construct scripts and commands for one explicitly selected target shell.
accepts_script_stdin = False
class-attribute
argv_invocation = True
class-attribute
command_separator
instance-attribute
context_order = ('env', 'cwd', 'command')
class-attribute
instance-attribute
cwd_guard = 'operator'
class-attribute
default_executable
instance-attribute
execution_epilogue = ''
class-attribute
instance-attribute
info_script
instance-attribute
line_terminator = '\n'
class-attribute
instance-attribute
multiline_source = False
class-attribute
name
instance-attribute
pathname = PurePath
class-attribute
instance-attribute
structured_command_prefix = ''
class-attribute
instance-attribute
__repr__()
Source code in src/hostctl/shell/_flavour.py
59 60 | |
__str__()
Source code in src/hostctl/shell/_flavour.py
400 401 | |
_raw_source(value)
staticmethod
Validate raw shell source, which is not a value.
A quoted value must not carry control characters -- that is what
_text enforces. Raw text is the opposite case: it is shell source
the caller wrote deliberately, where a newline is ordinary syntax
(and where a generated script, such as PowerShell's multi-line exit
epilogue, may legitimately be handed back for a second render). Only
NUL and the other non-whitespace control characters are refused --
nothing in any supported shell needs them and they defeat terminal
and log inspection.
Source code in src/hostctl/shell/_flavour.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
_separator_after(command)
The separator that may follow command without breaking it.
Source code in src/hostctl/shell/_flavour.py
334 335 336 337 338 | |
_text(value)
staticmethod
Normalize values and reject shell control characters.
Source code in src/hostctl/shell/_flavour.py
94 95 96 97 98 99 100 | |
argument(value, *, target=ShellTarget.NATIVE)
Quote one value for target, the parser that reads it next.
The default is every flavour whose shell hands tokens straight to
execve: there is no second parser, so quote() is the whole
answer. cmd and PowerShell override it, because on Windows the
child re-parses its own command line and the two layers disagree.
Source code in src/hostctl/shell/_flavour.py
131 132 133 134 135 136 137 138 139 140 141 142 | |
change_directory(cwd)
abstractmethod
Render a directory change which fails if the directory is absent.
Source code in src/hostctl/shell/_flavour.py
330 331 332 | |
cmdlet_command(value)
Render a Cmdlet; only PowerShell has cmdlets.
Source code in src/hostctl/shell/_flavour.py
176 177 178 | |
command(cmds, *, executable=None, cwd=None, env=None, login_shell=None)
abstractmethod
Build the command submitted to an SSH exec channel.
The string is parsed first by login_shell: None means nothing
parses it before the command line's own shell (as with
CreateProcess), "posix" an sh-family shell or fish, "cmd" and
"powershell" those shells. Each flavour's rule:
- POSIX and fish join the invocation with
login_join, which sh, bash, zsh, dash, ash and fish read back identically;cwdis part of the script. csh and tcsh are not supported. - PowerShell renders
-Commandfor acmdor POSIX login shell, the script on standard input (ShellCommand.stdin) when it cannot cross cmd.exe as an argument and the call leaves stdin free, and-EncodedCommand <base64 utf-16le>as the last resort. cmdescapes exactly one cmd layer and, forlogin_shell="cmd", one more for the login shell's own parse -- see the divergence ledger indocs/guide/contracts.md.
Source code in src/hostctl/shell/_flavour.py
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
command_target(values)
Which parser this command's arguments will meet.
Inferred where it is decidable -- cmd knows its own builtin list --
and NATIVE otherwise. It is deliberately NOT inferred for
PowerShell: whether Remove-Item is a cmdlet or an executable on the
guest's PATH cannot be known without a runspace, and guessing is the
class of mistake this package has been removing. A caller who knows
passes target= to :meth:argument directly.
Source code in src/hostctl/shell/_flavour.py
144 145 146 147 148 149 150 151 152 153 154 155 | |
command_text(value)
Source code in src/hostctl/shell/_flavour.py
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
environment_assignment(key, value)
abstractmethod
Render one validated environment assignment.
Source code in src/hostctl/shell/_flavour.py
180 181 182 | |
environment_script(env)
Convert an environment mapping into a standalone shell script.
Source code in src/hostctl/shell/_flavour.py
184 185 186 187 188 189 190 191 192 | |
escape_for_one_parse(text)
Escape text so ONE parse by this shell hands it back verbatim.
This is the OUTER layer, for a transport that delivers a finalized
command line to a login shell which parses it before the command
line's own shell ever runs. SSH is the case: AsyncSSH sends one
command string, and Windows OpenSSH's stock server hands it to
cmd.exe /c -- so a line from CmdShellFlavour.command(), which
already carries one cmd layer, is parsed by cmd twice.
The default refuses rather than guessing. A POSIX login shell receiving a POSIX-rendered line is the ordinary single-parse case and never reaches here; a flavour that genuinely has an escape for an extra parse implements it.
Source code in src/hostctl/shell/_flavour.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
group(command)
Wrap command so an operator applies to all of it, not its head.
Source code in src/hostctl/shell/_flavour.py
340 341 342 343 344 345 | |
invocation(script, *, executable=None)
abstractmethod
Build local-process argv which invokes this shell for one script.
Raises NotImplementedError when this shell cannot be invoked that
way -- see argv_invocation, and use command() instead.
Source code in src/hostctl/shell/_flavour.py
387 388 389 390 391 392 393 394 395 396 397 398 | |
join(values)
Join commands, preserving raw strings and explicit operators.
Source code in src/hostctl/shell/_flavour.py
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
join_cwd(changed, command)
Join cwd setup and payload with the shell's AND operator.
Reached only under cwd_guard = "operator". The payload is grouped
first: fused on directly, the AND bound to the first command only,
so cd /srv && a; b ran b in the login directory and exited 0 --
reachable through QGA (which always embeds cwd), fish or cmd over
SSH, and any Shell over a capability-less executor.
Source code in src/hostctl/shell/_flavour.py
347 348 349 350 351 352 353 354 355 356 | |
operator(value)
abstractmethod
Render one supported command operator.
Source code in src/hostctl/shell/_flavour.py
157 158 159 | |
quote(value)
abstractmethod
Quote one value for THIS SHELL's parser.
The syntactic layer only: enough that the shell sees one word and
expands nothing. What the token meets after that is
:meth:argument's question.
Source code in src/hostctl/shell/_flavour.py
122 123 124 125 126 127 128 129 | |
redirect(value, target, operand)
Render a REDIRECT or APPEND value together with its target.
operand is redirect_operand(target); target is passed as well so
a flavour can tell raw source from a file name.
Source code in src/hostctl/shell/_flavour.py
239 240 241 242 243 244 245 246 | |
redirect_operand(value)
Render the file name after > or >>.
Raw text stays source. A path, bytes or a one-element structured command is a name, quoted for the shell's own parser: the redirect target is read by the shell, never by a child's C runtime.
Source code in src/hostctl/shell/_flavour.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
script(cmds, *, cwd=None, env=None, epilogue=True)
Build a script, applying environment and cwd consistently.
epilogue=False leaves off the flavour's status epilogue, for a
script whose transport reports the status itself (wants_epilogue
answers that for a set of capabilities) or that is one line of a
persistent session.
Source code in src/hostctl/shell/_flavour.py
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |
structured_command(values)
Source code in src/hostctl/shell/_flavour.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 | |
hostctl.Shell(flavour, executor, *, cwd=None, env=None, encoding=None, errors=None)
Bases: Generic[_Result]
Bind one shell language to a host or to an executor.
Over a host, run() is that host's run(): the host renders exactly
one shell layer with its provider's capabilities, so host.shell.run(x)
hands the provider what host.run(x) does. Over an executor (a
callable such as LocalExecutor()), the shell renders the script itself
and sends it as a script string, as argv (args), or as a command line,
embedding cwd/env unless the executor carries them natively.
A Shell is not an Executor: calling it configures it, and a
positional argument raises TypeError.
Source code in src/hostctl/shell/_shell.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
_cwd = cwd
instance-attribute
_encoding = encoding
instance-attribute
_env = dict(env) if env else None
instance-attribute
_errors = errors
instance-attribute
_execute = executor
instance-attribute
_executor_accepts_cwd = ExecutorCapability.CWD in self.executor_capabilities
instance-attribute
_executor_accepts_env = ExecutorCapability.ENV in self.executor_capabilities
instance-attribute
_executor_accepts_options = any(item.kind is inspect.Parameter.VAR_KEYWORD for item in self._executor_parameters.values())
instance-attribute
_executor_parameters = inspect.signature(self._execute).parameters
instance-attribute
_flavour = flavour
instance-attribute
_host = None
instance-attribute
_session = None
instance-attribute
_spawn = spawn if callable(spawn) else None
instance-attribute
cwd = property(lambda self: self._cwd)
class-attribute
instance-attribute
encoding = property(lambda self: self._encoding)
class-attribute
instance-attribute
env = property(lambda self: self._env)
class-attribute
instance-attribute
errors = property(lambda self: self._errors)
class-attribute
instance-attribute
executor_capabilities = frozenset(published)
instance-attribute
flavour = property(lambda self: self._flavour)
class-attribute
instance-attribute
__call__(*args, cwd=None, env=_INHERIT_ENV, encoding=None, errors=None)
Configure: return a copy carrying these defaults (see configure).
Source code in src/hostctl/shell/_shell.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
__enter__()
Open a default session, so with host.shell as session: works.
The session is closed on exit. session(...) remains the way to pass
a command, cwd, env, terminal, or encoding; this is the no-argument
shorthand. A Shell is not reusable as a context manager while a
session it opened is still active -- each with opens its own.
Source code in src/hostctl/shell/_shell.py
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/shell/_shell.py
425 426 427 428 429 430 431 432 433 434 | |
_accepts_keyword(name)
Source code in src/hostctl/shell/_shell.py
198 199 200 201 202 203 | |
_resolve_cwd(cwd)
Source code in src/hostctl/shell/_shell.py
230 231 | |
_resolve_env(env)
Merge a per-call environment over this shell's default.
The default {} merges nothing, so a call that says nothing about the
environment inherits the shell's. A mapping merges over that default
per key: the call wins for keys it names, and default-only keys
survive, so one variable can change without restating the rest.
None declines the shell's configured defaults. It does not mean
an empty environment: the command still runs with whatever the host
provides on its own -- a login profile, rc files, the service
environment -- because nothing is sent to override it. Requesting a
genuinely empty environment is a separate feature that does not exist
yet.
Source code in src/hostctl/shell/_shell.py
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
configure(*, cwd=None, env=_INHERIT_ENV, encoding=None, errors=None)
Return a copy of this shell with additional defaults applied.
env merges over this shell's default the same way a per-call env
does, so configuring twice layers rather than replaces, and None
produces a copy carrying no environment default at all. The original
shell is left unchanged.
Source code in src/hostctl/shell/_shell.py
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 | |
run(*cmds, stdin=None, stdout=None, stderr=None, cwd=None, env=_INHERIT_ENV, capture_output=None, check=None, encoding=None, errors=None, input=None, timeout=_OMITTED, text=None)
Run shell commands, as one script, on the host or executor.
env merges over the shell's default per key; the default merges
nothing and so inherits it. Pass None to run without the shell's
configured environment, keeping whatever the host provides itself.
timeout=None is passed on as "no limit"; left out, the host's
configured default applies.
Source code in src/hostctl/shell/_shell.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | |
session(*cmds, executable=None, cwd=None, env=_INHERIT_ENV, terminal=None, encoding=None, errors=None)
Open a persistent process using this shell language.
env merges over the shell's default per key; the default merges
nothing and so inherits it. Pass None to open the session without
the shell's configured environment, keeping whatever the host
provides itself.
Source code in src/hostctl/shell/_shell.py
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | |
hostctl.ShellSession(flavour, process)
Bases: Process
A persistent process with shell-aware command submission.
Source code in src/hostctl/shell/_shell.py
38 39 40 | |
flavour = flavour
instance-attribute
process = process
instance-attribute
returncode
property
__enter__()
Source code in src/hostctl/shell/_shell.py
93 94 95 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/shell/_shell.py
97 98 99 100 101 102 103 | |
close()
Source code in src/hostctl/shell/_shell.py
90 91 | |
kill()
Source code in src/hostctl/shell/_shell.py
87 88 | |
read(size=-1)
Source code in src/hostctl/shell/_shell.py
63 64 | |
read_stderr(size=-1)
Source code in src/hostctl/shell/_shell.py
66 67 | |
resize(columns, rows, pixel_width=0, pixel_height=0)
Source code in src/hostctl/shell/_shell.py
72 73 74 75 76 77 78 79 | |
send(*cmds, cwd=None, env=None)
Render and submit commands using this session's shell language.
Source code in src/hostctl/shell/_shell.py
46 47 48 49 50 51 52 53 54 55 56 57 58 | |
send_eof()
Source code in src/hostctl/shell/_shell.py
69 70 | |
terminate()
Source code in src/hostctl/shell/_shell.py
84 85 | |
wait(timeout=None)
Source code in src/hostctl/shell/_shell.py
81 82 | |
write(data)
Source code in src/hostctl/shell/_shell.py
60 61 | |
hostctl.ShellOperator
Bases: Enum
A command operator, spelled by each shell flavour in its own syntax.
Place a member between two commands in a run()/script() call and
the flavour renders it: PIPE is | everywhere, AND/OR are
&&/|| in POSIX and PowerShell 7 but raise on PowerShell 5, which
has neither. A flavour that cannot express one refuses rather than
approximating it.
AND = 'and'
class-attribute
instance-attribute
APPEND = 'append'
class-attribute
instance-attribute
OR = 'or'
class-attribute
instance-attribute
PIPE = 'pipe'
class-attribute
instance-attribute
REDIRECT = 'redirect'
class-attribute
instance-attribute
SEQUENCE = 'sequence'
class-attribute
instance-attribute
hostctl.register_shell_flavour(value, *, name=None, replace=False)
Register a configured flavour instance for string-based selection.
Source code in src/hostctl/shell/__init__.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
hostctl.shell_flavour(value)
Normalize a public shell selection without inferring from the transport.
Source code in src/hostctl/shell/__init__.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
Consoles
hostctl.ConsoleStream
Bases: Protocol
A console's merged byte stream, as a profile drives it.
read returns up to size bytes (-1: whatever is available), or b""
when nothing arrived within timeout seconds: None is the stream's own
wait, 0 returns only what is already there. It raises ConnectionError
once the stream has ended. write delivers all of data or raises.
A stream may also offer reset_input_buffer(), which a profile calls to
discard bytes nobody is waiting for.
read(size=-1, *, timeout=None)
Source code in src/hostctl/console/_stream.py
23 24 | |
write(data)
Source code in src/hostctl/console/_stream.py
26 27 | |
hostctl.console_stream(process)
process as a ConsoleStream a profile can drive with a deadline.
A process whose read already takes timeout= (a SerialProcess, a
serial spawn() session) is returned as it is. Any other byte-mode
Process -- a QEMU console from open_serial(), a container or SSH
session opened with terminal=True -- is read by a background thread
from then on, so read it only through the returned stream: its read
must block until data arrives and return b"" only at the end.
Source code in src/hostctl/console/_stream.py
41 42 43 44 45 46 47 48 49 50 51 52 53 | |
hostctl.ConsoleProfile
Bases: Protocol
What a console profile implements: negotiate, send, and optionally run.
run returns the command's output and status, and is called only when
can_run is true. Every method takes the ConsoleStream to drive.
can_run
instance-attribute
encoding
instance-attribute
line_terminator
instance-attribute
negotiate(stream)
Source code in src/hostctl/console/_profiles.py
31 | |
run(stream, command, *, timeout=None)
Source code in src/hostctl/console/_profiles.py
37 38 39 40 41 42 43 | |
send(stream, command)
Source code in src/hostctl/console/_profiles.py
33 34 35 | |
hostctl.RawConsoleProfile
Raw byte console; it supports interactive sessions but not run.
can_run = False
class-attribute
instance-attribute
encoding = 'utf-8'
class-attribute
instance-attribute
line_terminator = b'\r\n'
class-attribute
instance-attribute
max_buffer = 64 * 1024
class-attribute
instance-attribute
shell_flavour = None
class-attribute
instance-attribute
negotiate(stream)
Source code in src/hostctl/console/_profiles.py
79 80 | |
run(stream, command, *, timeout=None)
Source code in src/hostctl/console/_profiles.py
90 91 92 93 | |
send(stream, command)
Source code in src/hostctl/console/_profiles.py
82 83 84 85 86 87 88 | |
hostctl.PromptConsoleProfile(prompt, *, login=(), line_terminator=b'\r\n', error_patterns=(), status_marker=None, reliable_status=False, max_buffer=64 * 1024, read_size=4096, wakeup=b'\r', echo=True, paging_prompt=None, paging_continue=b' ', paging_disable=None, max_paging_pages=32, terminal_setup=None, status_parser=None, login_timeout=10.0)
Configurable prompt/login framing for terminal-like devices.
A profile only advertises run when reliable_status is explicitly
enabled. Without a real status marker a prompt can delimit output, but it
cannot truthfully represent a process exit status.
Source code in src/hostctl/console/_profiles.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
_login_patterns = tuple(self._compile(step.expect) for step in self.login)
instance-attribute
_paging_prompt = self._compile(paging_prompt) if paging_prompt else None
instance-attribute
_prompt = self._compile(prompt)
instance-attribute
can_run = self.reliable_status and self.status_marker is not None
instance-attribute
echo = bool(echo)
instance-attribute
encoding = 'utf-8'
instance-attribute
error_patterns = tuple(self._compile(value) for value in error_patterns)
instance-attribute
line_terminator = line_terminator.encode() if isinstance(line_terminator, str) else bytes(line_terminator)
instance-attribute
login = tuple(step if isinstance(step, LoginStep) else LoginStep(step[0], step[1]) for step in login)
instance-attribute
login_timeout = float(login_timeout)
instance-attribute
max_buffer = max(1024, int(max_buffer))
instance-attribute
max_paging_pages = int(max_paging_pages)
instance-attribute
paging_continue = paging_continue.encode() if isinstance(paging_continue, str) else bytes(paging_continue)
instance-attribute
paging_disable = paging_disable.encode() if isinstance(paging_disable, str) else bytes(paging_disable) if paging_disable is not None else None
instance-attribute
read_size = max(1, int(read_size))
instance-attribute
reliable_status = bool(reliable_status)
instance-attribute
shell_flavour = None
class-attribute
instance-attribute
status_marker = self._compile(status_marker) if status_marker is not None else None
instance-attribute
status_parser = status_parser
instance-attribute
terminal_setup = terminal_setup
instance-attribute
wakeup = bytes(wakeup)
instance-attribute
_compile(value)
staticmethod
Source code in src/hostctl/console/_profiles.py
198 199 200 201 202 203 204 205 206 | |
_read(stream, remaining)
One backend read, bounded by what is left of the caller's budget.
Without the bound, run(timeout=) was only checked between reads,
so a SerialConfig(timeout=Timeout(read=None)) host -- a supported setting
that round-trips through the URI -- blocked inside the backend
forever instead of raising TimeoutExpired.
Source code in src/hostctl/console/_profiles.py
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 | |
_read_until(stream, expression, *, timeout, initial=b'', paging_stream=None, skip=None, floor=0)
Source code in src/hostctl/console/_profiles.py
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
_window(buffer, skip, floor)
Where this exchange's own output can start.
Everything before it is the command this profile just echoed back, or
an earlier page boundary -- text that must never terminate, abort or
forge the read. find is recomputed each pass because paging edits
the buffer underneath.
Source code in src/hostctl/console/_profiles.py
208 209 210 211 212 213 214 215 216 217 218 219 | |
drain(stream)
Discard whatever the previous exchange left in the stream.
Raw serial is one merged stream with no request/response
correlation, so bytes left by an earlier command -- a timed-out
run(), or the reply to paging_disable that negotiate() never
read -- were consumed by the NEXT run() and reported as its output
with returncode 0.
Source code in src/hostctl/console/_profiles.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
negotiate(stream)
Source code in src/hostctl/console/_profiles.py
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 | |
redact(transcript)
Remove secret=True login values from a console transcript.
Consoles echo what they are sent, so a transcript captured during
negotiate() holds the password itself -- and _read_until
attaches that transcript to the TimeoutError it raises, which
run() then copies onto TimeoutExpired.output: a value callers
routinely log. LoginStep.__repr__ goes out of its way to print
<redacted>; this is the same promise on the other exit.
Source code in src/hostctl/console/_profiles.py
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
resize(stream, columns, rows)
Source code in src/hostctl/console/_profiles.py
351 352 353 354 355 356 | |
run(stream, command, *, timeout=None)
One framed exchange, in its own window.
The window starts after this command is echoed back and ends at the first prompt following the completion marker. Nothing before it can terminate, abort or forge the exchange: a prompt-shaped or marker-shaped string in the echoed command or in the output belongs to the output, and only the marker the device appended last decides the status. Whatever the previous exchange left behind is discarded before the command is sent, and nothing is ever dropped silently.
Source code in src/hostctl/console/_profiles.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 | |
send(stream, command)
Source code in src/hostctl/console/_profiles.py
358 359 360 361 362 363 364 | |
hostctl.PosixConsoleProfile(*, login=(), login_timeout=10.0, wakeup=b'\n', read_size=4096, shell='posix', batch=_BATCH)
Framed commands on the terminal of a POSIX shell.
Needs sh-compatible syntax (sh, dash, bash, zsh, busybox ash, ksh) and
base64, mktemp, stty, cat and rm on the target. The command runs
in the console's own shell through eval, so cd and variables persist
between commands and exit ends the session.
shell names that shell's flavour ("posix", "bash", "zsh" or a
PosixShellFlavour); a host driving this profile quotes structured
commands and embeds cwd/env with it. Any other flavour raises
ValueError, since the framing itself is sh syntax.
batch is how many typed bytes may be in flight before the shell
acknowledges them (at least 1024, the default; None: no
acknowledgements). Keep the default on a serial line, whose 4 KiB input
queue drops what overflows it, and on a pty read and written from one
thread: a macOS pty stops taking input after about 1.3 KiB while its
echo goes unread (a Linux one takes 8 and 16 KiB). A stream that blocks
the writer and whose far end keeps reading the terminal (a web shell, an
SSH or container session) takes None, saving a round trip per KiB.
Source code in src/hostctl/console/_posix.py
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
_login_patterns = tuple(PromptConsoleProfile._compile(step.expect) for step in self.login)
instance-attribute
batch = batch
instance-attribute
can_run = True
class-attribute
instance-attribute
encoding = 'utf-8'
class-attribute
instance-attribute
line_terminator = b'\n'
class-attribute
instance-attribute
login = tuple(step if isinstance(step, LoginStep) else LoginStep(step[0], step[1]) for step in login)
instance-attribute
login_timeout = float(login_timeout)
instance-attribute
read_size = max(1, int(read_size))
instance-attribute
shell_flavour = flavour
instance-attribute
wakeup = wakeup.encode() if isinstance(wakeup, str) else bytes(wakeup)
instance-attribute
_acknowledged(stream, marker, deadline, frame)
Read until the shell prints marker; return what followed it.
Source code in src/hostctl/console/_posix.py
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 | |
_drain(stream)
Read and drop what the console has already sent, without waiting.
Raises OperationNotStartedError when the stream has failed: it is
only called before the command line is typed.
Source code in src/hostctl/console/_posix.py
506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 | |
_emit(sink, data)
staticmethod
Source code in src/hostctl/console/_posix.py
465 466 467 468 | |
_expect(stream, pattern, initial)
Read until pattern matches; return what followed the match.
Source code in src/hostctl/console/_posix.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
_prepare(stream, command, *, input, merge_stderr, deadline)
Type everything command needs before the line that runs it.
Returns the frame parser, what the console sent after the last
acknowledgement, and the command line still to write. Raises
OperationNotStartedError when the stream fails, and TimeoutError
(after a ^C) when deadline passes waiting for an acknowledgement:
either way the command line was never typed.
Source code in src/hostctl/console/_posix.py
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 | |
_read(stream, deadline, frame)
One bounded read; past the deadline, interrupt and raise.
Source code in src/hostctl/console/_posix.py
548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | |
_start(stream, command, *, input, merge_stderr, deadline)
Type command through the line that runs it.
Returns the frame parser and the (stdout, stderr) it already
framed. Raises what _prepare raises; a failure while typing the
command line itself is not a decline, since part of it may be typed.
Source code in src/hostctl/console/_posix.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | |
_type(stream, lines, *, drain_after=False)
Write lines that come before the command line; a failure ran nothing.
At most _WRITE bytes go in one write, and what the console sent is
read before the next (and after the last, with drain_after): a
terminal whose echo nobody reads stops reading its input, and the
blocked write would never return. Until the command line is typed,
that output is echo and prompts only.
Source code in src/hostctl/console/_posix.py
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 | |
execute(stream, command, *, input=None, merge_stderr=False, timeout=None, on_stdout=None, on_stderr=None)
Run command; return (stdout, stderr, status) as the program wrote them.
input is the command's whole stdin; without it stdin is /dev/null.
merge_stderr sends stderr into stdout (2>&1). on_stdout
receives stdout as it arrives, on_stderr stderr when the command
ends. On timeout a ^C is typed and TimeoutError raised, carrying
.output and .stderr so far. A stream failure before the command
line was typed raises OperationNotStartedError.
Source code in src/hostctl/console/_posix.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 | |
negotiate(stream)
Log in, then prove a POSIX shell answers a framed command.
Raises TimeoutError when a login step or the shell does not answer
within login_timeout, and ConsoleProtocolError when the shell
answers but the probe fails.
Source code in src/hostctl/console/_posix.py
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
redact(transcript)
transcript with every secret=True login value replaced.
Source code in src/hostctl/console/_posix.py
294 295 296 297 298 299 | |
run(stream, command, *, timeout=None)
Run command; return its stdout and stderr merged, and its status.
Source code in src/hostctl/console/_posix.py
353 354 355 356 357 358 359 360 361 362 363 364 | |
send(stream, command)
Type command and a newline, for an interactive session.
Source code in src/hostctl/console/_posix.py
348 349 350 351 | |
hostctl.ConsoleExecutor(stream_factory, *, profile=None)
Bases: StreamingExecutor
A subprocess-shaped executor over a POSIX shell's console.
stream_factory() returns the ConsoleStream of a console whose shell
is ready to type into (log in there, with profile.negotiate(stream),
when it needs it); it is called once per command. profile frames each
command (default PosixConsoleProfile(); pass one with batch=None for
a stream that cannot overflow). Wrap it in an ExecutorProvider: it
takes no native args, cwd or env, so a host renders one line of
shell text, which runs in a child shell of the console's.
Output is delivered as it arrives: stdout during the command, stderr
when it ends. One command holds the terminal from its first typed byte
until its status arrives; another call waits for it, within its own
timeout. OperationNotStartedError (a provider decline) is raised only
before the command line is typed: when stream_factory raises OSError
or TimeoutError, or the stream fails while the command's files are
typed. A failure after that is a ConnectionError.
After a timeout, and after a failure once the stream was obtained (a
decline included), _abandon(request, stream=) types ^C on that stream.
A timeout raises TimeoutExpired with orphaned=True: ^C stops only a
program that heeds it. Override _abandon to also drop stream,
returning False when that ends the command (closing a login session
hangs up its jobs). It runs before the terminal is released, so no other
command has typed into stream.
Source code in src/hostctl/console/_executor.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
_running = None
instance-attribute
_terminal = threading.Lock()
instance-attribute
executor_capabilities = frozenset()
class-attribute
instance-attribute
profile = profile
instance-attribute
stream_factory = stream_factory
instance-attribute
_abandon(request, *, stream=None)
Type ^C on stream, the command's terminal; True: it may still run.
stream defaults to the running command's. A write that fails is
ignored: a stream is often abandoned because it already failed.
Source code in src/hostctl/console/_executor.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
_owned(request)
Source code in src/hostctl/console/_executor.py
165 166 167 168 169 | |
_receive(request, wait)
Source code in src/hostctl/console/_executor.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
_release(request)
Source code in src/hostctl/console/_executor.py
171 172 173 174 175 | |
_send(request)
Source code in src/hostctl/console/_executor.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
_stop(request)
Source code in src/hostctl/console/_executor.py
151 152 153 154 155 156 157 158 159 160 161 162 163 | |
hostctl.LoginStep(expect, send, secret=False)
dataclass
One bounded expect/send step of a console login.
expect
instance-attribute
secret = False
class-attribute
instance-attribute
send
instance-attribute
__post_init__()
Source code in src/hostctl/console/_profiles.py
54 55 56 57 58 59 60 | |
__repr__()
Source code in src/hostctl/console/_profiles.py
62 63 64 65 66 | |
Exceptions
hostctl.HostctlError
Bases: Exception
Base of every exception hostctl raises of its own.
hostctl.TransportError
Bases: HostctlError, ConnectionError
The connection to the target failed or was never made.
hostctl.OperationNotStartedError(reason='operation was not started', *, cause=None)
Bases: TransportError
A provider declined before anything was sent to the target.
Nothing ran, so retrying the operation elsewhere is safe; this is the only
failure ordered provider fallback moves past. cause is the transport
error that made the provider decline, when there was one.
Source code in src/hostctl/exceptions.py
44 45 46 47 48 49 50 51 52 | |
cause = cause
instance-attribute
reason = reason
instance-attribute
hostctl.QGADisconnectedError
Bases: TransportError
The guest-agent transport disconnected before producing a reply.
hostctl.ProtocolError
Bases: HostctlError, ConnectionError
The peer answered, but not in the protocol's expected shape.
hostctl.QGAProtocolError
Bases: ProtocolError
The guest agent returned malformed or inconsistent protocol data.
hostctl.ConsoleProtocolError
Bases: ProtocolError
The console did not complete the expected protocol exchange.
hostctl.QGATimeoutError
Bases: HostctlError, TimeoutError
A guest-agent request did not complete before its deadline.
hostctl.QGACommandError(error_class, description, *, data=None)
Bases: HostctlError
The guest agent refused a command with a structured error.
error_class and description are the agent's own fields; data holds
any extra detail it sent.
Source code in src/hostctl/exceptions.py
82 83 84 85 86 87 88 89 90 91 92 | |
data = dict(data or {})
instance-attribute
description = description
instance-attribute
error_class = error_class
instance-attribute
hostctl.BundleError
Bases: HostctlError, RuntimeError
A deploy bundle cannot be built correctly from what is installed here.
Raised before anything is sent to the target: a distribution with a compiled extension or nothing importable, a missing ignore file, a source tree without an importable package.
hostctl.MissingExtraError(extra, feature, *, python=None, distribution='hostctl')
Bases: HostctlError, ImportError
A feature needs an optional extra that this installation lacks.
extra is the name to install (pip install "DISTRIBUTION[EXTRA]") and
feature what needed it. distribution is the package whose extra it is:
a library built on hostctl raises this for its own extras. With python,
the extra cannot be installed on this interpreter at all: it names the
oldest Python the extra supports.
Source code in src/hostctl/exceptions.py
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
distribution = distribution
instance-attribute
extra = extra
instance-attribute
feature = feature
instance-attribute
python = python
instance-attribute
Provider composition
hostctl.ExecutorProvider(name, executor, *, capabilities=None, probe=None)
Bases: _Provider
Named callable executor with a conservative probe hook.
Source code in src/hostctl/provider/_contracts.py
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | |
__call__ = execute
class-attribute
instance-attribute
executor = executor
instance-attribute
execute(command, *args, **options)
Source code in src/hostctl/provider/_contracts.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
hostctl.PathProvider(name, factory, *, capabilities=None, probe=None)
Bases: _Provider
Named path factory. path must return a pathapi.Path.
Source code in src/hostctl/provider/_contracts.py
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
DEFAULT_CAPABILITIES = frozenset(('stat', 'scandir', 'open', 'open_read', 'open_write', 'read', 'write', 'exists', 'is_file', 'is_dir', 'mkdir', 'chmod', 'unlink', 'rmdir', 'rename', 'symlink_to', 'readlink', 'utime', 'hardlink_to'))
class-attribute
instance-attribute
factory = factory
instance-attribute
path(*segments)
Source code in src/hostctl/provider/_contracts.py
294 295 296 297 298 299 300 301 302 303 304 305 | |
hostctl.ProviderProbe(availability, reason='', capabilities=frozenset(), system_hint=None)
dataclass
availability
instance-attribute
capabilities = frozenset()
class-attribute
instance-attribute
reason = ''
class-attribute
instance-attribute
system_hint = None
class-attribute
instance-attribute
usable
property
hostctl.ProviderSelection(provider, trace, generation=0, pinned=False)
dataclass
generation = 0
class-attribute
instance-attribute
pinned = False
class-attribute
instance-attribute
provider
instance-attribute
trace
instance-attribute
hostctl.SelectionStep(provider, availability, reason='', capabilities=(), chosen=False, generation=0, pin=False)
dataclass
One candidate a selection considered, in provider precedence order.
provider is the provider's name and reason its refusal or probe
reason, both redacted. availability is "unavailable" for a provider
that declined before dispatch this generation. capabilities is what the
probe stated, sorted.
availability
instance-attribute
capabilities = ()
class-attribute
instance-attribute
chosen = False
class-attribute
instance-attribute
generation = 0
class-attribute
instance-attribute
pin = False
class-attribute
instance-attribute
provider
instance-attribute
reason = ''
class-attribute
instance-attribute
hostctl.ProviderDetail(kind, provider, availability, reason='', capabilities=(), system_hint=None)
dataclass
A provider's availability as host.provider_details reports it.
Read without dispatching; provider and reason are redacted, and
capabilities is the effective set (declared, narrowed by the probe).
availability
instance-attribute
capabilities = ()
class-attribute
instance-attribute
kind
instance-attribute
provider
instance-attribute
reason = ''
class-attribute
instance-attribute
system_hint = None
class-attribute
instance-attribute
hostctl.ProviderSelector(providers=())
Ordered selector which never retries a dispatched operation.
Source code in src/hostctl/provider/_selector.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
_SECRET_ASSIGNMENT = re.compile('(?i)(?<![A-Za-z0-9])((?:[A-Za-z0-9_.$:\\-]*[_.$:\\-])?' + _SECRET_KEYWORD + '[A-Za-z0-9_.\\-]*)\\s*[=:]\\s*((?:\\"[^\\"]*\\"|\'[^\']*\')+|[^&\\s,;]+)')
class-attribute
instance-attribute
_SECRET_FISH_SET = re.compile('(?i)(?<![A-Za-z0-9])(set(?:\\s+-[A-Za-z]+)*\\s+)((?:[A-Za-z0-9_]*[_])?' + _SECRET_KEYWORD + '[A-Za-z0-9_]*)\\s+((?:\\"(?:\\\\.|[^\\"\\\\])*\\"|\'(?:\\\\.|[^\'\\\\])*\')+|[^&\\s,;]+)')
class-attribute
instance-attribute
_SECRET_KEYWORD = '(?:password|passwd|pwd|secret|token|api[_-]?key|key|credential)'
class-attribute
instance-attribute
_SECRET_USERINFO = re.compile('(?i)\\b([a-z][a-z0-9+.\\-]*://[^/\\s:@]+):[^/\\s@]+@')
class-attribute
instance-attribute
_attempt_trace
property
writable
This thread's accumulating trace for the operation it is running.
_decline_causes = {}
instance-attribute
_declined = {}
instance-attribute
_fallback_hook = None
class-attribute
instance-attribute
_generation = 0
instance-attribute
_lock = threading.RLock()
instance-attribute
_probe_cache = {}
instance-attribute
_traces = threading.local()
instance-attribute
declines
property
Providers that refused before dispatch this generation, by name.
Maps provider name to the redacted refusal reason. A copy: mutating the result does not change selector state.
generation
property
The current connection generation; probes are cached per value.
providers = tuple(providers)
instance-attribute
trace
property
The accumulated trace for the operation currently being selected.
_effective(provider, probe)
staticmethod
Source code in src/hostctl/provider/_selector.py
210 211 212 213 214 215 216 217 218 | |
_readmit_earlier_declines(excluded, capability)
Forget declines that the running operation did not make.
A name the caller excluded is one it tried and saw refuse, so only
declines outside excluded came from an earlier operation.
Source code in src/hostctl/provider/_selector.py
326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
_record(step)
Record one step, a later one for a provider superseding the earlier.
Ordering follows first appearance, so the trace still reads in provider precedence order after a decline rewrites an entry.
Source code in src/hostctl/provider/_selector.py
352 353 354 355 356 357 358 | |
_served(provider)
Note that provider, chosen by this thread's last selection, carried
the operation; reports to _fallback_hook when providers ranked above
it were skipped as unavailable (a capability skip is routing, not a
fallback).
Source code in src/hostctl/provider/_selector.py
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | |
decline(name, reason='declined before dispatch', *, cause=None)
Record that a provider refused before dispatch this generation.
A provider which raised OperationNotStartedError proved that nothing
started, so it is safe to skip it for the rest of this generation
rather than re-attempting it on every later operation. The record is
cleared by :meth:invalidate along with the probe cache.
Source code in src/hostctl/provider/_selector.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | |
effective_capabilities(provider)
What provider can do now: its declared set, narrowed by its probe.
A probe may narrow the declared capabilities and must not invent any,
so the answer is the intersection when the (cached) probe states a set,
and the declared set when it states none. A provider declaring the
path wildcard declares everything, so what its probe states stands.
Source code in src/hostctl/provider/_selector.py
200 201 202 203 204 205 206 207 208 | |
invalidate()
Start a new generation, dropping cached probes and declines.
Source code in src/hostctl/provider/_selector.py
385 386 387 388 389 390 391 392 393 | |
probe(provider)
Return a provider probe cached for this selector generation.
A provider that declined before dispatch this generation reports as
unavailable with the refusal reason, overriding the cached probe.
The probe describes whether the transport could work; the decline is
the newer and stronger evidence that right now it does not, so any
caller reading availability (provider_details, capabilities)
sees the refusal instead of a stale available.
Source code in src/hostctl/provider/_selector.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 | |
redact(value)
classmethod
Return a diagnostic string with credential-like values removed.
Best effort, and deliberately so. This recognizes credentials that
announce themselves -- a password=/token=/api_key=
assignment, or the userinfo half of a scheme://user:secret@host
URI. A bare positional secret (mysql -p hunter2, an argv element
that is only a token) carries no marker and cannot be detected by
inspection. Treat debug output as sensitive.
Source code in src/hostctl/provider/_selector.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
retry_declined()
Forget every refusal recorded this generation.
Not needed to recover a host: select() already re-admits an earlier
refusal when nothing else can serve. This is for a caller that knows
the declined provider is back (sshd restarted) and wants it preferred
over a fallback that is still serving.
Source code in src/hostctl/provider/_selector.py
76 77 78 79 80 81 82 83 84 85 86 | |
select(*, capability=None, exclude=(), pin=False)
Source code in src/hostctl/provider/_selector.py
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | |
hostctl.SessionInitializer(initialize, timeout=None)
dataclass
Optional post-connect bootstrap hook receiving the connected system host.
initialize
instance-attribute
timeout = None
class-attribute
instance-attribute
__call__(host, **options)
Run the hook, passing timeout only to a hook that accepts it.
The documented example,
SessionInitializer(lambda h: h.run("sudo -v"), timeout=10), takes
no timeout keyword, and passing one would raise at connect().
The value is advisory: it is handed to a hook that declares it, and
nothing here enforces a deadline. A hook that wants one applies it to
the calls it makes (host.run(..., timeout=...)).
Source code in src/hostctl/provider/_contracts.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
hostctl.register_system_provider(name, resolver, *, replace=False)
Register a provider descriptor every system host accepts.
resolver(host) returns the (executor, path) pair the descriptor
offers, either of them None. A class's own provider_resolvers win over
this registry. A name registered already raises ValueError unless
replace=True.
Source code in src/hostctl/system/_resolvers.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | |
hostctl.system_provider_names()
Every globally registered provider descriptor, sorted.
The registered names plus the hostctl.system_providers entry points,
which are listed and not imported. A host class may add or hide names
with its own provider_resolvers.
Source code in src/hostctl/system/_resolvers.py
70 71 72 73 74 75 76 77 78 79 | |
hostctl.host_schemes()
Every scheme HostConfig(uri) can dispatch, sorted.
The schemes of the registered configs, plus the names of the
hostctl.configs entry points, which are listed and not imported.
Source code in src/hostctl/_registry.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
hostctl.CompositePosixPath(*segments, **kwargs)
Bases: _CompositePathMixin, PosixPathname, Path
A provider-selecting path with POSIX syntax on every client OS.
Source code in src/hostctl/paths/_mixin.py
72 73 74 | |
__slots__ = ('_provider', '_backend_path', '_providers', '_selector', '_factory', '_pinned', '_selection_trace')
class-attribute
instance-attribute
hostctl.CompositeWindowsPath(*segments, **kwargs)
Bases: _CompositePathMixin, WindowsPathname, Path
A provider-selecting path with Windows syntax on every client OS.
Source code in src/hostctl/paths/_mixin.py
72 73 74 | |
__slots__ = ('_provider', '_backend_path', '_providers', '_selector', '_factory', '_pinned', '_selection_trace')
class-attribute
instance-attribute
Connection strings
hostctl.ConnectionString(value, *, scheme=None, host=None, port=None, username=None, password=None, extras=None, path=None, query=None, fragment=None, defaults=None)
dataclass
A connection target parsed from whatever a user typed.
ConnectionString("wss://root:pw@nas:8443/api") parses, and so do a bare
host and a host:port once a scheme is supplied from outside the string:
ConnectionString("nas", scheme="wss") and
ConnectionString("nas:8443", defaults={"scheme": "wss"}). Without one,
ConnectionString("nas") and ConnectionString("nas:8443") raise
ValueError: nothing in the text says which scheme was meant. An opaque
URI with no host, local: or serial:COM3, parses and has an empty host.
Beyond that, the input must be a valid URI, and a malformed one
raises -- an unclosed IPv6 literal, a non-numeric port, or a password
written with an unescaped /, ? or # (each ends the authority, so
what precedes it is read as a port and fails to cast).
Reserved characters in a password must be percent-encoded, and one
that is not gets no rescue attempt. There is no rescue to make: a
password may itself contain @ and :, so no rule recovers where an
unescaped one ended. Those two do parse -- the last @ and the
first : win, as in any authority, giving pa@ss and pa:ss -- but
that is the URI grammar being applied, not a guess about the secret.
Guessing here would be guessing about what to connect to, so a raise is
the answer. :func:redact_uri is the other job and keeps its own rules:
stripping a secret out of text that may not parse at all, for a log line
nothing connects to. It does not over-redact a well-formed URI either --
it guesses only once the text has failed to parse.
Every field can also be supplied directly, and three layers decide each
one: an explicit argument wins, then whatever the string carried, then
defaults.
So scheme=, port=, and the rest are overrides, not defaults --
ConnectionString("wss://nas", scheme="ssh") is ssh, the same way
port= beats a port written in the string. Supplying a value only when
the string omits one is exactly what defaults is for:
ConnectionString("nas", scheme="wss") # wss://nas
ConnectionString("wss://nas", scheme="ssh") # ssh://nas
ConnectionString("wss://nas", defaults={"scheme": "ssh"}) # wss://nas
ConnectionString("nas:9", scheme="wss", port=1) # port=1 beats :9
defaults accepts a mapping or another ConnectionString, so a partially
filled one expresses "these settings unless the string says otherwise":
profile = ConnectionString("wss://root@nas", port=443)
ConnectionString("other", defaults=profile) # wss://root@other:443
Only fields a ConnectionString actually carries count as defaults, so an
empty path/query/fragment never overrides.
A port carries its own resolution strategy, wherever one is accepted --
as port=, in defaults, or written in the string. It may be:
- an
int, the port itself; - a callable
scheme -> port | None, asked once the scheme is settled; - anything indexable by scheme (a plain mapping included), so a caller passes its whole table without pre-selecting an entry;
None, meaning nothing was supplied here, so the next layer decides;False, meaning no port and no lookup -- stop here.
When no layer supplies one, netimps.get_default_port resolves the
scheme. It knows schemes the system services database does not
(ws/wss, socks5), and an application can register more with
netimps.register_port.
ConnectionString("nas", scheme="wss") # :443
ConnectionString("nas", scheme="wss", port=8443) # :8443
ConnectionString("nas", scheme="wss", port={"wss": 9}) # :9
ConnectionString("nas", scheme="wss", port=lambda s: 9) # :9
ConnectionString("nas", scheme="wss", port=False) # no port
A scheme a table or callable does not know is not an error -- it means
"no conventional port for this one", and the netimps fallback then
applies. A resolver that answers False declines that fallback, the same
as passing False directly.
The host keeps the spelling it was given. urlsplit().hostname is
case-folded, which is right for resolution and wrong for text rendered
back to a caller, so nasA stays nasA in host and in str(). DNS
treats the two as one name, so nothing about reaching the target changes.
Credentials never render. str() and repr() both emit the redacted
form -- the password is removed rather than masked, so the output stays a
valid, reusable connection string that cannot round-trip a wrong
credential. password is excluded from the generated repr, so a value
reaching a log line or a traceback frame does not leak it.
A password may carry credential extras after a newline, exactly as
:func:parse_credentials describes; extras holds them and password is
just the password. The separator may be written raw, since characters
urlsplit would silently delete are percent-encoded before parsing.
Source code in src/hostctl/uri/_connection.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
authority
property
The authority as it renders, without any password.
extras = _dc.field(default_factory=dict, repr=False, compare=False)
class-attribute
instance-attribute
fragment = ''
class-attribute
instance-attribute
host
instance-attribute
is_local
property
Whether this names the local machine, without resolving anything.
Deliberately does no I/O: this is called while a configuration is built, and that is network-free. Resolving would block, and would raise on a name that does not resolve -- so it could not even be used defensively.
netimps.is_local_host answers: loopback and addresses assigned to
this machine (10.0.0.5 is local when it is one of this host's own),
localhost, and this machine's own name. Any other name is not
resolved. An empty host (local:, serial:COM3) is local too, which
is_local_host would not say.
password = _dc.field(default=None, repr=False)
class-attribute
instance-attribute
path = ''
class-attribute
instance-attribute
port = None
class-attribute
instance-attribute
qsl
property
Query pairs in order, keeping repeats.
query = ''
class-attribute
instance-attribute
scheme
instance-attribute
username = None
class-attribute
instance-attribute
__hash__()
Hashable, as frozen=True advertises.
The generated __hash__ covered extras, a plain dict, so
{ConnectionString(t) for t in inventory} raised TypeError:
unhashable type: 'dict'. Hashing the rendered target keeps hash and
equality consistent: extras is credential material carried
alongside the password and is excluded from both.
Source code in src/hostctl/uri/_connection.py
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
__repr__()
Source code in src/hostctl/uri/_connection.py
341 342 | |
__str__()
Source code in src/hostctl/uri/_connection.py
338 339 | |
geturl()
The credential-free connection string, valid and reusable.
Source code in src/hostctl/uri/_connection.py
332 333 334 335 336 | |
query_val(name, default=None)
The last value for name, or default. Last wins, as in a URI.
Source code in src/hostctl/uri/_connection.py
370 371 372 373 374 375 376 | |
replace(**changes)
A copy with changes applied.
Built field by field rather than through dataclasses.replace, which
would re-invoke __init__ -- and __init__ parses a string, so it
does not accept the field names.
Source code in src/hostctl/uri/_connection.py
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | |
hostctl.redact_uri(uri)
Strip any password from uri, leaving a valid, reusable URI.
scheme://user:secret@host becomes scheme://user@host. The password is
removed rather than masked: a placeholder would make the URI round-trip
into a wrong credential if anything fed the rendered form back in, and
would not be a real password anyway. What comes out is the same canonical,
credential-free string a config renders, so it is safe to log, repr, or
hand back to HostConfig.
A URI with no password is returned unchanged.
A well-formed URI is never over-redacted. When the URI parses, what
urlsplit says is the authority is the authority, so an @ anywhere
else -- a mailbox in a path, ssh://nas:22/mail/user@example.com -- is
left alone. Guessing is confined to input that does not parse, where
there is nothing better to go on: see below.
This never raises. It is meant for error messages and log records, where
failing to render a diagnostic would be worse than rendering an odd one.
Characters urlsplit would delete (tab, CR, LF) are percent-encoded first,
the same as during dispatch, so a password written with a raw newline is
still recognized and removed rather than partly surviving into the output.
Source code in src/hostctl/uri/_parse.py
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | |
hostctl.parse_credentials(password)
Split a password field into the password and any trailing extras.
A newline separates the password from additional credential values, one
per line, each key:value:
"hunter2" -> ("hunter2", {})
"hunter2\notp:123456" -> ("hunter2", {"otp": "123456"})
"hunter2\notp:123456\nrealm:CORP" -> ("hunter2", {"otp": ..., "realm": ...})
A bare name with no : is a flag -- it maps to the empty string, exactly
as name: does:
"hunter2\ninteractive" -> ("hunter2", {"interactive": ""})
This is how a second factor reaches a transport through a single password field -- a URI's userinfo, an environment variable, a prompt -- without every caller inventing its own encoding. A newline is assumed not to be a valid password character, which is the same assumption pytruenas makes.
Names are casefolded and stripped of surrounding whitespace. A value is
everything after the first :, taken verbatim -- it may contain
colons, and its leading and trailing whitespace is preserved, because a
secret may legitimately begin or end with a space and silently trimming
one would fail authentication with no visible cause. Blank lines are
ignored.
Source code in src/hostctl/uri/_parse.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | |
hostctl.strict_uri_credentials(credentials, allowed)
Reject credentials which the selected implementation does not accept.
A key whose value is None is not a credential -- nothing was supplied --
so it is ignored rather than refused. Counting keys made
HostConfig("local:", password=None) fail, which is exactly the shape a
generic caller produces when it forwards an optional argument.
Source code in src/hostctl/uri/_parse.py
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | |
hostctl.strict_uri_query(parsed, allowed, *, repeatable=())
Parse one selected implementation's query without ambiguity.
A repeated key is an error by default, so ?shell=a&shell=b is not a
coin flip; an unknown key is an error too, so a misspelt
?exectuor=ssh fails here rather than as a missing capability later.
repeatable names keys that are ORDERED LISTS rather than settings --
SystemConfig's executor and path -- and their values come back as
a tuple in the order they were written. Declaring them here lets every
config share this one parser.
Source code in src/hostctl/uri/_parse.py
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 | |
hostctl.uri_host(host)
Bracket an IPv6 literal for use in a URI authority.
Source code in src/hostctl/uri/_parse.py
30 31 32 | |
hostctl.uri_hostname(parsed)
The host as it was written, not as urlsplit case-folded it.
urlsplit().hostname is lowercased by design -- DNS is case-insensitive,
so folding is right for resolution. It is wrong for text a caller will
see again. A config built from a URI stores this value and renders it back
through connection_uri, so reading .hostname there would make the
library echo a spelling the operator never typed, and nasA would reach
logs and HostInfo as nasa.
Use this in _from_parsed_uri wherever a host is stored. Presence checks
(if not parsed.hostname) can keep using .hostname -- emptiness does not
depend on case. Resolution is unaffected either way: DNS, SSH, and WinRM
all treat the two spellings as one name.
Returns "" for a URI with no host, matching hostname or "".
hostname is a case-folded substring of the authority (after any
userinfo), so locating it recovers the original text.
Source code in src/hostctl/uri/_parse.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | |
Composing transports
hostctl.ssh_providers(config)
Build an executor and path provider sharing one SSH transport.
This is the supported way to compose SSH into a host you assemble
yourself, rather than taking the finished PosixHost that
SSHConfig._create_host() returns:
executors, paths = [], []
run_provider, path_provider = ssh_providers(config.ssh)
executors.append(run_provider)
paths.append(path_provider)
Both providers must share one transport: that is what makes them share
a connection and a lifecycle, and SSHExecutorProvider is the one that
owns close. Assembling them by hand from two transports type-checks and
silently opens two connections, only one of which is ever closed -- which
is precisely why this returns a pair instead of exposing the transport.
Source code in src/hostctl/transports/ssh/_providers.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | |
hostctl.winrm_providers(config)
Build an executor and path provider sharing one WinRM transport.
The SSH counterpart, hostctl.transports.ssh.ssh_providers, documents why this
returns a pair rather than exposing the transport: both providers must
share one, and building them separately silently opens two.
Source code in src/hostctl/transports/winrm/_providers.py
93 94 95 96 97 98 99 100 101 102 103 | |
Transfers and checksums
hostctl.ProgressReader(reader, callback, *, total=None)
Wrap a binary reader and report (bytes_read, total_bytes).
Source code in src/hostctl/sync.py
112 113 114 115 116 117 118 119 120 121 122 123 124 | |
bytes_read = 0
instance-attribute
callback = callback
instance-attribute
closed
property
reader = reader
instance-attribute
total = total
instance-attribute
__enter__()
Source code in src/hostctl/sync.py
193 194 195 | |
__exit__(exc_type, exc_value, traceback)
Source code in src/hostctl/sync.py
197 198 | |
__getattr__(name)
Source code in src/hostctl/sync.py
200 201 | |
__iter__()
Source code in src/hostctl/sync.py
174 175 | |
__next__()
Source code in src/hostctl/sync.py
177 178 179 180 181 | |
_count(read)
Source code in src/hostctl/sync.py
126 127 128 | |
close()
Source code in src/hostctl/sync.py
190 191 | |
read(size=-1)
Source code in src/hostctl/sync.py
130 131 132 133 | |
read1(size=-1)
Source code in src/hostctl/sync.py
139 140 141 142 143 | |
readable()
Source code in src/hostctl/sync.py
183 184 | |
readinto(buffer)
Source code in src/hostctl/sync.py
145 146 147 148 149 150 151 152 153 154 | |
readinto1(buffer)
Source code in src/hostctl/sync.py
156 157 158 159 160 161 162 | |
readline(size=-1)
Source code in src/hostctl/sync.py
164 165 166 167 | |
readlines(hint=-1)
Source code in src/hostctl/sync.py
169 170 171 172 | |
hostctl.host_checksum(*hosts, algorithm='md5', chunk_size=1024 * 1024)
Build a PathSyncer checksum using execution beside owned paths.
More than one host may be supplied for a cross-host sync. Paths which do
not belong to any supplied host are hashed through their binary open()
contract, preserving interoperability with arbitrary pathapi
implementations.
Source code in src/hostctl/sync.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
hostctl.stat_checksum(entry)
Return the cached size and modification time without reading content.
This is rsync's quick check: no content is read and no command is run. Two caveats decide whether it suits a given sync.
It can miss a change which preserves both size and modification time.
It settles only where the target can take a modification time.
PathSyncer copies the source's modification time onto a target that
supports pathapi's Utime, so a repeated sync there is a no-op: local,
SSH and WinRM paths do, a QEMU guest path with a guest helper does, and a
container path does when the container has touch or
powershell.exe. Elsewhere a file copied lands with a fresh
modification time and compares unequal on the next run -- the sync
re-copies it.
Where the target cannot hold the time, use :func:host_checksum when
hostctl's own copies must converge to a no-op.
Source code in src/hostctl/sync.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
Deploying Python
hostctl.deploy needs the deploy extra; see
Deploying Python.
hostctl.deploy.deploy(host, source, path, *, mode='pyz', package=None, extras=(), skip=(), force=False, python='python3')
Install source at path on a POSIX host and say how to run it.
For a distribution name, python on the target is asked which
distributions it has, and only the rest of the closure ships, markers
evaluated for the target. mode="pyz" installs one zipapp file at
path; mode="dir" a tree at path with bin/<package>. The
payload is reproducible: when path already holds a deploy with the same
digest nothing is written, unless force. A path that exists but
was not written by a deploy is never replaced, force or not.
package, extras and skip are as for :func:bundle; skip
does not apply to a :class:SourceTree.
Raises ValueError for a relative path or a bad mode/package,
BundleError before anything is sent when the bundle cannot be built,
NotImplementedError for a host without POSIX paths, FileExistsError
for a foreign path, and subprocess.CalledProcessError when a step on
the target fails.
Source code in src/hostctl/deploy/__init__.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | |
hostctl.deploy.bundle(source, destination, *, mode='pyz', package=None, extras=(), skip=(), python='python3')
Write a self-contained bundle of source at destination.
source is an installed distribution's name (its whole dependency
closure ships, markers evaluated for this interpreter) or a
:class:SourceTree. mode="pyz" writes an executable zipapp file;
mode="dir" writes a directory holding lib/ and the launcher
bin/<package>. package is the import name run (default: the
distribution name with - as _; required for a tree). extras
are the distribution's extras to include; skip names distributions to
leave out. python is the interpreter the bundle names. Returns the
written path. Raises BundleError for a distribution with a compiled
extension or nothing importable, ValueError for a bad mode or
package.
Source code in src/hostctl/deploy/__init__.py
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |
hostctl.deploy.Deployment(path, digest, written, command)
dataclass
What :func:deploy did.
command is the argv that runs the deployed program on the target:
host.run(Exec(*deployment.command, *args)).
command
instance-attribute
digest
instance-attribute
path
instance-attribute
written
instance-attribute
hostctl.deploy.SourceTree(root, *, ignore_files=None, ignore_patterns=(), pythonpath=None)
dataclass
A working tree to ship as-is, filtered as a clone of it would be.
Its own dependencies are not vendored: that needs them installed here,
which a tree source exists to avoid. A tree ships only as mode="dir".
Source code in src/hostctl/deploy/__init__.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
ignore_files
instance-attribute
ignore_patterns
instance-attribute
pythonpath
instance-attribute
root
instance-attribute
_import_roots(package)
The tree's import roots for package, relative to lib/.
Source code in src/hostctl/deploy/__init__.py
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
files()
[(arcname, source)], each arcname under the root's own name.
Source code in src/hostctl/deploy/__init__.py
66 67 68 69 70 71 72 73 74 | |
requirements(extras=(), *, include=(), exclude=())
The dependency names the tree declares, read without installing.
From pyproject.toml (dependencies plus each of extras),
else requirements.txt. include/exclude take a name or a
bracketed "[extra]"; exclude wins.
Source code in src/hostctl/deploy/__init__.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
Aliases and constants
Every usable host.path() returns a pathapi.Path; annotate with that.
CommandLike (str | pathlib.PurePath | pathapi.Pathname) is the
command type accepted by Executor.
ExecutorCapability is the enum whose .ARGS, .CWD, and .ENV members
declare native executor support. Its members subclass str, so
ExecutorCapability.CWD == "cwd": capability sets are compared as strings
throughout, which lets providers advertise transport-specific tokens
("runspace") alongside the named members in one vocabulary.
POSIX_SHELL and POWERSHELL are the built-in shell-flavour instances;
BASH, ZSH, FISH, CMD, and PWSH are the other ready-to-use flavours.
Any of them can be passed wherever a ShellFlavour is expected.
has_ssh() and has_psrp() report whether the ssh and psrp extras are
installed, without importing them; a missing extra otherwise raises
MissingExtraError (an ImportError) naming it where it is used. __version__ contains the
installed package version.
ShellSelection and PathnameSelection (in hostctl.host) are what every
config's shell= and pathname= accept.