2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-08-03 16:07:00 +00:00
Commit Graph

6551 Commits

Author SHA1 Message Date
Jarrod Johnson d7dcb07a3f Implement deployment.storage
This is an attribute for a node to indicate preferences for storage.

For now, 'm2' policy will hit m.2 and mirroring kits.
2026-07-28 15:02:27 -04:00
Jarrod Johnson 3501f70c37 Merge pull request #247 from Obihoernchen/unsquashfs
Use multi-threaded unsquashfs to extract untethered images
2026-07-28 08:59:38 -04:00
Jarrod Johnson 035d6849e8 Merge pull request #257 from Obihoernchen/lenovo-async
Fix the NextScale SMM web path on the asyncio port
2026-07-28 08:48:50 -04:00
Jarrod Johnson 86e4603b82 Merge pull request #258 from Obihoernchen/imgutil-async
imgutil: fix async-port fallout in the image pack/capture path
2026-07-28 08:45:18 -04:00
Jarrod Johnson 46296d4751 Merge pull request #259 from Obihoernchen/versioning
Derive build versions from a tracked VERSION file
2026-07-28 08:37:53 -04:00
Markus Hilger ab13ec9e94 Remove duplicate "dracut_install lsmod ethtool" 2026-07-28 02:04:00 +02:00
Markus Hilger 67e84f15f8 Keep the root filesystem guard reachable when extraction fails
source_remote imageboot.sh is the last thing the diskless cmdline hook
runs, so returning early on a failed extraction ended the hook and left
dracut to time out. Falling through instead reaches the existing
/sysroot/sbin/init guard, which reports the failure and holds the node so
it stays reachable over ssh, as it did before extraction was checked.
2026-07-28 01:49:38 +02:00
Markus Hilger 62465812a3 Degrade gracefully when squashfs-tools is missing
imageboot falls back to cp when unsquashfs is unavailable, but the build
side did not: a bare dracut_install/copy_exec aborts initramfs generation
when the binary is absent, and the capture prerequisite check refused to
capture the image at all.

Mark the initramfs copies optional and report the missing package as an
advisory rather than a hard prerequisite, so such images still build and
capture, just without the faster extraction path. Widen the EL check to
every release past el8 so future ones inherit it.
2026-07-28 01:49:01 +02:00
Markus Hilger d9da502fbe Copy LICENSE into confluent_osdeploy before leaving the directory
The copy ran after the cd to the repo root, so it read ../LICENSE from
outside the checkout and never placed the file in confluent_osdeploy/. The
tarball went out without it and the spec's %install, which does
"cp LICENSE" after %setup cds into the unpacked directory, failed.
imgutil/buildrpm already copies before its cd; do the same here.

The aarch64 spec has its LICENSE lines commented out, so only the x86_64
build broke, but the copy was equally wrong in both scripts.
2026-07-27 20:19:39 +02:00
Markus Hilger a9d7b67929 Derive build versions from a tracked VERSION file
Release tags do not live on master: 3.15.2 through 3.15.6 were tagged on branch
3.15, so git describe reaches only 3.15.1 and dev builds were stamped
3.15.2.dev<n>. Besides being confusing, rpm and dpkg both rank the released
3.15.6 above that, so a dev package will not install over a released one.

Add a top-level VERSION file naming the release the branch is working toward
(4.0.0 on master) and a mkversion helper that stamps packages from it, keeping
the tag-derived value as a floor so a forgotten bump cannot go backwards.
mkversion also replaces the block copy-pasted into seven build scripts, and
makesetup no longer writes a per-package VERSION file, so the stale checked-in
confluent_common/VERSION goes with it.
2026-07-27 20:06:22 +02:00
Markus Hilger 4f112fb78d Skip the profile manifest when the server libraries are absent
confluent_imgutil does not depend on confluent_server, and the yaml
import is optional too, yet both capture and pack dereference osimage
and yaml unconditionally when writing manifest.yaml.  With
confluent_osdeploy present but the server absent that raises rather
than producing a profile.

Guard the manifest on both being importable and say so, since rebase
is what the manifest exists for.  The yaml fallback now binds None
instead of leaving the name undefined.

The two call sites carried the manifest write verbatim in both, so fold
them into one function rather than duplicate the guard as well.
2026-07-27 18:49:48 +02:00
Markus Hilger 9956845009 Do not block the import poll loop with time.sleep
osimport polls import progress from a coroutine, so a blocking sleep
between reads stalls the whole client loop.  It was the only use of time
in the script, so the import goes with it.
2026-07-27 18:49:48 +02:00
Markus Hilger 3e7da14a9a Test the import drain loops for an error before a percentage
Both loops that read the importer's output test for a percentage first,
so an ERROR: line whose text carries a % takes the percentage branch and
float() raises instead of the error being reported.  The import target
name can carry one too, and that one is user supplied.  importmedia runs
as a bare task, so the exception is swallowed and the client polls a
phase that never advances.

Test for ERROR: first and treat an unparsable percentage as no
percentage.  Set percent on the error path of the second loop as well,
as the first already does.
2026-07-27 18:49:48 +02:00
Markus Hilger b081c17b55 Let the import drain loop accumulate a line
The loop that drains the importer's remaining output reads a byte at a
time but clears currline on every iteration, one level out from where
the earlier loop clears it.  currline is therefore never longer than a
single byte, so the percentage and ERROR: branches can never match and
the tail of an import is silently discarded.

Clear it only once a line has been consumed, as the earlier loop does.
2026-07-27 18:49:48 +02:00
Markus Hilger f2c74b0be3 Fingerprint installation media off the event loop
scan_iso walks an entire ISO with blocking libarchive reads, yielding
only once per entry, and the header-sum branch of fingerprint reads the
whole file with no yield at all.  Both run in the daemon, reached from
MediaImporter.init on every fingerprint and importing request.

The scan costs about 8us per entry and is indifferent to media size,
since libarchive seeks past file data rather than reading it: measured
at 80ms for 10k entries whether the image is 0.2 GB or 8.8 GB, and at
310ms for 40k.  The header-sum branch is the one that scales with size,
reading a multi-gigabyte image end to end.

Make the pair plain functions and hand them to a thread instead.
2026-07-27 18:49:48 +02:00
Markus Hilger 547ecf16d4 Release the crypt device if encrypt_image is interrupted
Nothing unwound the loop device and dm-crypt mapping when the copy loop
raised, so interrupting a pack stranded both, still holding the profile's
rootimg.sfs.

Tear them down from a finally.  The retry loop moves with them, so also
honour its tries counter, as unpack_image already does; spinning forever
inside a finally would hang the interrupt it is meant to clean up after.

A bounded retry loop can also give up, and the detach that follows would
then fail with EBUSY and, raising from a finally, replace the exception
that brought us here.  Warn and leave both in place instead.
2026-07-27 17:01:49 +02:00
Markus Hilger 4e9052012f Keep imgutil pack and capture synchronous
Both functions became coroutines solely to await one get_hashes call,
but their bodies are long stretches of blocking work: mksquashfs, the
encrypt_image copy loop, rsync, ssh and osdeploy.

From Python 3.11 on, asyncio.run installs a SIGINT handler that cancels
the main task and returns rather than raising, so an interrupt is only
noticed at the next await.  Interrupting a pack during mksquashfs
surfaced as a CalledProcessError from the dying child instead of a
KeyboardInterrupt, and with a base profile, where nothing is ever
awaited, pack carried on and published the profile before exiting.

Run the loop only around the call that needs it.
2026-07-27 17:01:33 +02:00
Markus Hilger 38be080bec Accept a command list in check_call
check_output unwraps a single list argument, check_call never did, so
callers passing a list hit a TypeError out of create_subprocess_exec.
Two callers do: the genisoimage run behind Windows profile imports,
where an except Exception swallows the failure and the boot.iso is
silently missing, and the nodeconfig run in discovery, which takes out
automatic node configuration on discovery outright.
2026-07-27 17:01:33 +02:00
Markus Hilger aba564914f Limit imgutil manifest hashes to the profile source
capture and pack hash the whole profile directory, which by that point
holds rootimg.sfs, the kernel and the distribution initramfs.  rebase
only ever looks up entries that came from the profile source directory,
so the image blobs cost gigabytes of hashing for nothing.

Pass the source directory as the filter, as generate_stock_profiles
already does.  Older manifests keep working, since rebase reads their
entries with a default.
2026-07-27 17:01:18 +02:00
Markus Hilger 1a9613f22e Hash profile files in larger chunks
The asyncio port added an await between every 2048 byte read, which
roughly doubled the cost of hashing.  imgutil runs entire packed images
through this, and the server pays it on rebase and media import.

Read a megabyte per iteration instead.  That still yields hundreds of
times per gigabyte, so the event loop stays responsive, and sha512 is
independent of the read size, so existing manifests remain valid.
2026-07-27 17:01:18 +02:00
Markus Hilger ec7b96ecfb Decode SMM response bodies before raising them
grab_response_with_status hands back bytes, so every failure path put a
b'<status>error</status>' repr in front of the operator rather than what
the SMM said.  Decode at the raise, replacing rather than failing on a
body that is not valid utf8.  The bodies still reach fromstring() as
bytes, which is what lxml wants when the xml carries an encoding
declaration.
2026-07-27 16:22:46 +02:00
Markus Hilger 4d75c444ca Ride out a transient bad status while firmware applies
The poll loop spends its retry budget on a poll that goes unanswered but
aborted the update on the first non-200, even though an SMM restarting
its web service part way through the apply keeps answering, with
whatever its httpd has to say, before it stops answering at all.  Give a
bad status the same budget as a dead connection.
2026-07-27 16:22:00 +02:00
Markus Hilger f5a90f3e35 Fail set_user_priv on a rejected privilege change
Every other /data call checks the status, this one discarded the
response, so an SMM that refused the user record was reported to the
caller as a successful privilege change.
2026-07-27 16:13:42 +02:00
Markus Hilger 6593863988 Re-establish an SMM web session the chassis has dropped
Staleness is judged by age alone, so a session the SMM ended on its own
reached the operator as a raw error body instead of being retried.
Route the /data calls through a helper that logs back in and retries
once on a 401, which is how the SMM answers once a session is gone.

A hostname or domain write does not end the session, measured on a
DW612S at firmware 1.18, so this covers what the chassis drops by
itself, not a self-inflicted loss.
2026-07-27 16:13:42 +02:00
Markus Hilger c3d6e0ae58 Clear the firmware poll retry budget after a good poll
The counter is there to ride out a few unanswered progress polls, but
nothing ever cleared it, so three failures spread across a long apply
exhausted it and aborted an update that was still making progress.
2026-07-27 16:13:42 +02:00
Markus Hilger d665064dbd Hold the SMM web session across long operations
A firmware update posts on one session for the minutes its apply loop
runs, and an FFDC collection downloads on the session it acquired, but
wc() judges a session by its age alone, so a settings call arriving
thirty seconds in logged that session out from underneath them.  Flag
the long operations the way the IMM and XCC handlers already do and
leave their session in place.
2026-07-27 16:13:42 +02:00
Jarrod Johnson 29ba1d8515 Merge pull request #254 from Obihoernchen/exclude
Add exclude option to confluentdbutil
2026-07-27 09:51:37 -04:00
Jarrod Johnson 27b173f739 Merge pull request #256 from Obihoernchen/client_async
Fix async-port regressions in confluent_client
2026-07-27 08:14:59 -04:00
Markus Hilger f8ea1adec7 Keep the nodediscover CSV import going past a failed assignment
gather propagates the first exception and leaves its siblings running,
so a transport level failure against one node ends the import with a
traceback while the rest of the batch is cancelled at loop shutdown.
The forked children used to contain such a failure to their own node.
assign_macs already reports an error response itself, so this is the
connection dropping rather than the server refusing the assignment.

Collect the exceptions instead, report each one and count it towards
the exit code.  Schedule the assignments as tasks while doing so, since
the plain coroutines are left unawaited if defining a later node raises
before the gather is reached.
2026-07-27 06:15:45 +02:00
Markus Hilger a619b6ed6f Bound the sessions the nodediscover CSV import opens
Replacing the forked children with a gather kept their fan-out: every
row of the import file gets a session of its own and they all start at
once, so a large file opens a local socket and a server side session
task per node simultaneously.

Hold a semaphore for the duration of each node's assignment instead, so
a finished node's session is dropped before the next one starts.  Also
build that session once per node rather than once per MAC, and say why
the caller's session is not reused, which was self evident while this
ran in a forked child.
2026-07-27 06:15:32 +02:00
Markus Hilger 18c24effc6 Initialize the scan total in nodediscover register
register_endpoint primes current but not total, so a first response
without a count field goes straight to

  UnboundLocalError: local variable 'total' referenced before assignment

on the elif.  Start at zero, which skips the progress line until the
server does report a count.
2026-07-27 06:15:08 +02:00
Markus Hilger 1031bad407 Keep the SMM web session across settings operations
Every getter and setter logged out on the way out, which nulled the
cached client and made the session cache inert on exactly the paths it
was meant to serve: a single nodeconfig walk of ntp costs two full
logins for the read and one per server for the write, each of them a
fresh TLS handshake plus, on firmware that omits st2, two extra page
fetches to scrape the tokens.

Leave the session in place and let wc() dispose of it once it expires.
This also stops one coroutine's logout from invalidating the session
another coroutine just fetched and is about to post with.
2026-07-27 04:56:32 +02:00
Markus Hilger 474e2bd975 Drop unreachable web client check in get_diagnostic_data
wc() either returns a client or propagates the exception raised while
logging in; it cannot return None the way connect() could.
2026-07-27 04:56:32 +02:00
Markus Hilger 6e6cbce0a2 Do not report an interrupted firmware update as complete
The retry counter is there to ride out a few unanswered polls, but
exhausting it broke out of the loop with complete still unset and fell
through to the 'complete' return, so an SMM that stopped answering
part way through an apply was reported to the operator as updated.
Raise instead; a genuine finish still leaves the loop on the progress
reaching 100.
2026-07-27 04:56:32 +02:00
Markus Hilger 5135a6cd3d Make the SMM web session cache safe to share
Now that the expiry comparison actually caches a client, the session it
holds is shared, so tearing it down and replacing it needs the same care
the IMM handler already takes:

Dispose of an expired session with a logout instead of dropping the
reference, otherwise every refresh leaves an authenticated session
behind on an SMM that only has a handful of slots.  That logout has to
tolerate a session the SMM has already reaped, hence the except.

Guard the login itself, so two coroutines arriving at an empty or
expired cache do not both log in and orphan one of the two sessions.

Stamp the vintage once the login round trips are done rather than
before, so a slow SMM cannot hand back a client that is already expired.
2026-07-27 04:56:32 +02:00
Markus Hilger ca81907d25 Restore SMM web request semantics lost in the async port
The old WebConnection.request() added a
'Content-Type: application/x-www-form-urlencoded' header to any POST
carrying a body, but grab_response_with_status() only sets a content
type for dict payloads, so the SMM login and every /data form POST now
go out as text/plain.  This is not a fix for an observed failure: an SMM
running FPC variant 38 was measured accepting a text/plain login exactly
as readily as a urlencoded one.  It restores the header the synchronous
code always sent and that the TSM and IMM handlers still set explicitly,
rather than relying on every SMM firmware level being equally lax about
what it will parse.

Also stop hard failing on responses the synchronous code discarded on
purpose.  'set=securityrollback:1' is only understood by newer SMM2
firmware.  And /data/logout answers 401 once the session is gone, as
measured on that same SMM, so raising on a non-200 there turns a
completed hostname, domain or NTP operation into a spurious error.
2026-07-27 04:55:57 +02:00
Markus Hilger 8817ee6deb Await NextScale SMM settings operations
The SMM hostname, domain, and NTP helpers looked synchronous even though their web transport is asynchronous. Removing awaits in the Lenovo OEM handler therefore returned unresolved coroutine work instead of completed settings results.

Convert the SMM settings and logout helpers to the asynchronous web interface, validate HTTP status responses, and await each operation from the OEM handler so callers only observe completed results.
2026-07-27 04:54:44 +02:00
Markus Hilger fbbeda6c86 Fix NextScale asynchronous web client
The NextScale SMM path still used the removed http.client-style interface against the asynchronous WebConnection implementation. Login, configuration, diagnostic, and firmware operations consequently called unavailable methods or left request coroutines unresolved.

Make web-client creation asynchronous, migrate the affected requests to grab_response_with_status(), and await the cached client accessor. Correct the cache expiry comparison so fresh authenticated clients are reused and stale clients are renewed.
2026-07-27 04:54:08 +02:00
Markus Hilger 53f1d4a7c2 Do not block the event loop with time.sleep in the async clients
nodediscover's rescan poll and nodeconsole's screenshot refresh both slept
with time.sleep inside a coroutine.  In nodeconsole --video that stops the
input handler and the VNC streaming tasks for the whole interval.
2026-07-27 01:54:24 +02:00
Markus Hilger 15670f0ab1 Put the local socket in non-blocking mode in the async client
_connect_unix left the socket blocking, while _connect_tls sets a zero
timeout, so every loop.sock_recv and sock_sendall against the local
socket ran the blocking call inline and stalled the whole event loop.
nodeconsole --video showed this most clearly: a power action opens its
own session, so the tiles stopped refreshing and keystrokes went
unhandled until the BMC finished.

asyncio only enforces this in debug mode, where the client failed
outright with ValueError: the socket must be non-blocking.  The
descriptor passing retries in asynctlvdata also assume a non-blocking
socket, since they wait for BlockingIOError.
2026-07-27 01:54:01 +02:00
Markus Hilger 64cfad09af Complete the async port of the nodegroup attribute paths
simple_nodegroups_command awaited the async generators returned by read
and update, which raises

  TypeError: 'async_generator' object can't be awaited

and printgroupattributes was left synchronous, iterating one of those
generators with plain for.  Neither is reachable yet, since nodeattrib
only ever passes a noderange and nodegroupattrib still uses the
traditional client, but they are the paths nodegroupattrib will use once
it is ported.
2026-07-27 01:52:09 +02:00
Markus Hilger 550751d0ff Fix the file descriptor send retry in asynctlvdata
When sendmsg() reports EAGAIN, _sendmsg rescheduled itself with

  loop.add_reader(fd, _sendmsg, loop, fut, sock, fd)

which waits for the socket to become readable rather than writable, and
passes four of the six required arguments, so the callback raised
TypeError once it did fire.  Wait for writability and pass the message
and descriptors through.

Also skip the work in _recvmsg if the future was cancelled while waiting
for data, as _sendmsg already does, so a cancelled read does not end in
InvalidStateError from set_result.

This module is imported by the server as well, so both paths are reached
by the daemon whenever a descriptor is passed over the local socket.
2026-07-27 01:51:53 +02:00
Markus Hilger 7ebc1dc616 Fix nodediscover CSV import in the async port
import_csv was left with several synchronous idioms:

- search_record is a coroutine function, but was called without await.
  The returned coroutine is always truthy, so the rescan on incomplete
  discovery data never happened, and iterating the result raised
  TypeError: 'coroutine' object is not iterable
- the node creation loop iterated an async generator with plain for
- the per-node discovery assignment was forked off with os.fork() while
  the event loop was running, and the child then built a fresh session
  on the inherited selector

Assign discovery entries with asyncio.gather instead of a forked child,
which keeps the assignments concurrent and lets their exit codes
propagate.  The forked child always ended in sys.exit(0), so its
accumulated errorcode was discarded.
2026-07-27 01:51:16 +02:00
Markus Hilger 4197bd9118 Fix nodediscover register and subscribe in the async port
register_endpoint and subscribe_discovery were left as plain functions
iterating the async client generators, so nodediscover register,
subscribe and unsubscribe all failed immediately with

  TypeError: 'async_generator' object is not iterable
2026-07-27 01:50:24 +02:00
Markus Hilger db303ca014 Allocate a fresh node index when merging a backup
A node imported by a merge was assigned a free index and then had it
overwritten by the index carried in the backup, which may already belong
to a node in the target database.  Keep the allocated index instead; a
full restore still honors the dumped index.
2026-07-25 05:17:46 +02:00
Markus Hilger fa3d1ca388 Add exclude option to confluentdbutil
The -x/--exclude option drops matching node and node group attributes
from a dump, restore, or merge, so a backup can leave out dynamic state
such as deployment.state_last_updated or data that should not travel with it.
Patterns use shell-style wildcards, and a bare namespace such as net
excludes every attribute below it.  The node "groups" and "id.index"
attributes and the node group "noderange" attribute are always retained
so that a restore can still reconstruct group membership and node index
assignments.
2026-07-25 05:17:22 +02:00
Jarrod Johnson 3f2ad75b6d Change output of the nodedeploy timestamp
The 'updated' could be confused for OS updates or similar.
2026-07-24 16:23:27 -04:00
Jarrod Johnson bf195bfdeb Fix stray typing in nodedeploy 2026-07-24 15:10:06 -04:00
Jarrod Johnson a65583c325 Clean up some headers missed in the rebase to aio http 2026-07-24 12:38:33 -04:00
Jarrod Johnson 9fa89d712a Format timestamp consistent with nodeveentlog 2026-07-24 12:05:58 -04:00