Repository navigation
v0.3.0
Added
- QGA paths have metadata and namespace operations.
QemuHost.path()
now supportsstat,iterdir,mkdir,unlink,renameandchmod
through a guest-side helper built during discovery -- hostctl previously
shipped none, so a caller had to write one and every QGA path reported
degraded. The helper is POSITIVELY PROBED: GNUstat -c/find -printf
on a POSIX guest, PowerShell on a Windows one. A guest that fails the
probe keeps content access and loses only what genuinely needs a helper.
QemuConfig(path_helper=...)still overrides. SshConfig(login_shell="auto"|"cmd"|"powershell"|"posix")says what
parses the command string on arrival. Windows OpenSSH's stock server
hands it tocmd.exe /c, so acmd-dialect command line is parsed by cmd
twice; unescaped, 4 of 8 adversarial values survived and two of the
failures were command injection.autoreads the dialect.ShellFlavour.argument(value, *, target=ShellTarget...)separates
quoting for the shell's parser from quoting for whatever reads the token
next -- a cmd builtin, a child's argv parser, PowerShell's binder, or the
program slot.quote()remains the syntactic layer.
Removed
SerialTransportand itshostctl.executorexport. It duplicated
SerialProcess's locked, deadline-clamping byte path and had no caller
outside its own tests, so the two could drift with nothing to notice.
SerialProcesskeeps the job -- it is what every console profile reads
through -- and gained thewrite(timeout=)it lacked.SerialConfig.usernameandSerialConfig.password. They were stored,
whitelisted for URI dispatch, and read by nothing: console credentials live
in the profile'slogin=steps. An explicit one is now refused rather than
silently ignored, andHOSTCTL_PASSWORDis no longer offered to aserial:
URI.
Changed
- The
pathlib_nextfloor moves to>=0.9.10in bothdependenciesand
thesshextra. 0.9.4 through 0.9.9 cannot be used: those releases
decide "same file" and "overlapping trees" by falling back to path
equality, which compares(type, segments)and knows nothing about
hosts, so copying/etc/app.conffrom one host to the same path on
another was refused withOSError [Errno 22] Source and target are the same file(andPathSyncerwith "source and target overlap"). 0.9.10
adds the_same_filesystem()hook this release answers. LocalExecutormergesenvover the inherited environment instead of
replacing it, so the documented cross-transport rule -- "envis additive
to the provider's environment" -- now holds locally too. A child no longer
starts withoutPATH,HOMEorSystemRootbecause one variable was set.- A serial
run()frames each command as its own profile exchange and
refuses a structured (argv) command: a serial host names no shell flavour,
so it has no quoting rule to apply, and joining commands with;put two
on one line where most device consoles read a single malformed one.
stdin=andbufsize=raise instead of being ignored, asinput=already
did. - A serial
spawn(encoding=...)reads text, matching the SSH and QEMU
console adapters; without either text keyword it still reads bytes. - QEMU
dialect="auto"/path_flavor="auto"need positive evidence of the
guest's family --guest-get-osinfo, or a family-exclusive command in
guest-info-- and raise naming the explicit settings when neither is
available. An agent withguest-get-osinfoblocklisted used to be read as
POSIX, silently, including on Windows. QemuHost.info().os_familyis a family (linux,windows,
freebsd), taken fromkernel-name. It carriedguest-get-osinfo'sid,
so the same machine reportedlinuxover SSH andubuntuover QGA.- Each guest-agent request is bounded by
agent_timeout, separately from
a command's owntimeout=: an agent that stops answering is a
ConnectionErrorsaying so, not a command timeout ten minutes later. - A truncated QGA
run()result warns.stdout_truncated/
stderr_truncatedwere set and documented nowhere, so a shortened
transcript looked complete. hostctl shellopens a console that cannot allocate a terminal, so a
serial:URI works;hostctl cp local:/tmp/xis accepted, the
spelling every other subcommand documents; andhostctl --helpcarries the
URI forms, theURI:PATHgrammar, theHOSTCTL_PASSWORDrule and the exit
codes.asyncsshandlibvirt-pythonare bounded (>=2.14,<3,>=9.0,<12);
thedevextra now pulls every transport, sopip install -e ".[dev]"is
the whole development environment.LocalHostis aSystemHost. It duplicated the provider selection,
failover loop, capability computation and dispatch ladder, and the two
copies diverged. Two consequences for callers:
LocalHost().path()with no segments no longer returns the absolute
working directory -- it returns a path with no segments added, like
every other transport, which still resolves against the process's working
directory. A local path is still a plainpathlib_nextpath, not a
composite.
Fixed
- A transfer between two hosts is no longer mistaken for a file copied
onto itself.CompositePosixPath/CompositeWindowsPathnow answer
pathlib_next's_same_filesystem()with the host's provider set, so
two hosts are two namespaces even when a path is spelled identically on
both, while two paths on one host still are one — which keeps the guard
that refusescopy()of a file onto itself. A.via()pin selects a
route to the host, not a different host, and answers accordingly. - The SFTP leg verifies the server's host key.
SshConfig.connect_opts()
omittedknown_hostswhenever it held its()default. asyncssh reads a
missing key as "resolve known_hosts the usual way", sorun()verified the
server — butpathlib_next's SFTP connect seedsknown_hosts=None
(verification off) for options it is not handed, sopath()accepted any
host key, including an actively substituted one, and sent it the configured
password. Opting out is still possible and now explicit:known_hosts=None. ConnectionStringparsesuser:password@host. A scheme-shaped username
madeurlsplitreadroot:hunter2@nasas the schemeroot, so the password
was never recognised: it stayed in the path, wherestr()andrepr()
rendered it verbatim, the host came out empty, andis_localreported a
remote machine as local. Without a scheme to assume, a target carrying
credentials is now refused rather than guessed at, and both error messages
redact (root:<redacted>@nas).- An args-capable executor gets one shell layer, not two.
SystemHost
fed a finished shell command line back intoflavour.invocation(), which
takes a script. On Windows the outer PowerShell re-parsed the inner
-Commandstring, sowindows://node?executor=localreported 0 for
cmd /c exit 3—check=Truepassed for a failing command — and quoted
text and variables came back mangled or empty. - cmd.exe builtin operands are quoted, and
setno longer escapes inside
quotes. cmd splits a builtin's operands on,,;and=as well as
whitespace, sodel /q a b.txtdeletedaandb.txtand left the named
file. Separately,set "KEY=VALUE"caret-escaped inside a quoted span,
where cmd does not process carets:100%reached the child as100^%,
%OS%still expanded, and a"in the value ended the assignment early.
An empty value remains inexpressible in cmd, and is now documented as such. - PowerShell 5 native arguments survive the C runtime. PS 5.1 rebuilds the
command line for a native program and leaves embedded quotes alone, so
run(("robocopy", src, dst, name))withname = 'my file" /MIR "z'handed
robocopy/MIR— mirror mode, which deletes files in the destination. Empty
arguments were dropped and a trailing backslash swallowed the next argument.
PowerShell 7 is unaffected and keeps the plain literal. - A transfer to another backend no longer renames inside the source host.
ssh_host.path('/srv/export.csv').move(LocalPath('/home/op/export.csv'))
issued an SFTP rename on the server: the file left/srv, never arrived,
and no error was raised. Astrdestination — documented bypathlib_next
— also raisedTypeErroron every transport, and a cross-providermove()
aborted instead of falling back to copy + remove. All three now work. - Docker file modes are translated. The archive stat header carries Go's
os.FileMode, used verbatim as a POSIXst_mode:is_file()was False for
every ordinary file andis_dir()raisedOverflowError: mode out of range
for a directory, which also killed any recursive copy. - An unreachable QGA socket is reported as a transport failure. It escaped
as a bareFileNotFoundError, indistinguishable from the guest file being
absent — so an append across a guest reboot staged an empty buffer and
truncated the guest file when the agent came back. - Native WinRM reports the remote exit status. The provider did not
declaremanages_status, so PowerShell's; exit $LASTEXITCODEepilogue was
appended and itsexitended the remote pipeline before the
__HOSTCTL_LASTEXITCODE__marker could be emitted. A command that failed by
exit status reported 0 andcheck=Truepassed. - One unsupported operation no longer takes a path provider out of
service. ANotImplementedErrorfrom a retry-safe call was recorded as
a decline on the host's shared provider selector, which every later
operation then consulted: after a backend refusedsamefile()— it needs
st_dev/st_ino, which many remote stats lack — the next read on any
path of that host failed withno path provider supports open_read. The
refusal is now scoped to the call that raised it.OperationNotStarted
still declines the provider for the generation, as before. - A
cmd.exeat a path containing a space is launchable. The program
slot of a Windows command line is read byCreateProcess, not by cmd, and
rendering it with cmd's caret quoting made Windows look for a program named
^-- measured asFileNotFoundError: [WinError 2]. - A structured PowerShell command can bind a named parameter.
["Remove-Item", "-LiteralPath", path]rendered every token as a quoted
string, which PowerShell's binder reads positionally: measured against a
realpowershell.exe, it returned 1 and left the file in place. A token
that is exactly a parameter name is now unquoted. ShellSession.send()does not append a separator the text already has,
sosend("echo hi;")is notecho hi;;andsend("sleep 5 &")is not
sleep 5 &;.zshquoting covers a leading=, which zsh expands to a program path
where POSIXshleaves it alone.PowerShellFlavour(7, executable=...)is namedpwsh. The name was
derived only when no executable was given, so an explicit path left the
flavour identifying aspowershell.Shell(..., env={})forwards no environment, rather than an empty one
that starts a child with nothing.normalize_inputcoversbytearrayandmemoryview-- the deadlock
it exists to prevent, for the two buffer types it did not check.- Container archive reads no longer lie about links, sizes or errors: a
single-member hardlink archive raisesOSError(ENOENT)instead of
tarfile.StreamError, a hardlinked file in a listing reports its real
size, the archive root (".") is listable, and every Docker status code
maps through one helper that populateserrno. - A container exec stream no longer returns
''mid-character, and a
closed container process reads EOF instead of raising the operating
system's raw socket error. - QGA failures speak one error vocabulary: a guest error is classified on
thestrerrortail (a file namednotfound.logwas reported missing
whatever actually failed),host.run()of a missing guest program raises
FileNotFoundError, libvirt-relayed guest errors stay guest errors, and a
guest error no longer tears down and re-handshakes the connection. - One transport per QEMU host. Lazy initialisation was unguarded:
measured with six threads, six transports were built where one was wanted,
each SSH one opening a connection nobody owned. A path handed out before
close()now refuses instead of silently reconnecting. - A serial login failure raises
ConsoleProtocolError, not a bare
TimeoutErrorthat slipped past every documented handler, and a transcript
attached to an error hassecret=Truelogin values redacted -- consoles
echo what they are sent. - A QGA reader is seekable where the agent advertises
guest-file-seek,
sozipfileandtarfilework against a guest path, and an exclusive
open("x")is refused atopen()rather than after the whole payload has
been staged. - A
cmd-dialect command sent over SSH to Windows is escaped for the
login shell that receives it. Two of the eight adversarial values were
command injection, not corruption. SshConfig("ssh://host")raises instead of making the URI the
hostname, which surfaced much later as an unresolvable name. The
constructor takes a host;HostConfig(uri)takes a URI.
Full Changelog: v0.2.7...v0.3.0