2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-29 00:31:09 +00:00

Compare commits

...

127 Commits

Author SHA1 Message Date
Jarrod Johnson 24cb05e535 Fix name of ssh in various ubuntu scripts 2026-05-13 13:52:05 -04:00
Jarrod Johnson 72b95abb4f Add missing syncfiles examples to ubuntu profiles 2026-05-13 13:51:59 -04:00
Jarrod Johnson 57a170d0d8 Implement a headless mode
For automation, this can make more sense.
2026-05-12 11:23:37 -04:00
Jarrod Johnson daeabc6fe5 Add expression support to the nodeconsole automation 2026-05-11 16:51:01 -04:00
Jarrod Johnson 28a8f6f0d6 Provide automation facility for nodeconsole
Allow nodeconsole to walk console according to a script
2026-05-11 13:55:54 -04:00
Jarrod Johnson 7542897b43 Normalize perms to int or None 2026-05-07 09:44:00 -04:00
Jarrod Johnson c69952265f Permit override of unix ownership/permissions on sockets
If an environment knows more specifically what should have access in terms of group, allow service.cfg to indicate.
2026-05-07 08:44:16 -04:00
Jarrod Johnson 01cc86fa55 Add a '-r' argument to refresh site contents
If an environment manually manages all materials,
provide -r to let
them request packing of those materials
without trying to generate any of the content.
2026-05-06 08:46:31 -04:00
Jarrod Johnson d6e3c7e837 Backport cert fix 2026-05-05 16:26:49 -04:00
Jarrod Johnson dcb6aeca65 Add ca-only policy
This policy forces CA validation every time.

This also checks things like date validity.
2026-05-05 14:41:02 -04:00
Jarrod Johnson 7bc76b62e6 Backport CA policy changes 2026-05-05 11:31:26 -04:00
Jarrod Johnson db313628c5 Include aarch64 names for key libraries in ubuntu diskless 2026-05-01 14:25:18 -04:00
Jarrod Johnson f260323d2f Fix missing ubuntu diskless content 2026-05-01 12:14:13 -04:00
Jarrod Johnson d60bc7f524 Bring chrony fixes to other scripts 2026-04-30 11:24:20 -04:00
Jarrod Johnson ff0d4cdadf Fix diskless profiles for chrony.conf modification 2026-04-30 11:24:15 -04:00
Timothy Middelkoop db6475c4da Fix el8/el9 hook paths corrupted by symlinked el10 in aarch64 spec
In confluent_osdeploy-aarch64.spec.tmpl, el10 was created as a symlink
to el8, so the subsequent `mv el10/initramfs/usr el10/initramfs/var`
inadvertently renamed el8's usr directory, leaving el8 and el9 (also
symlinked to el8) with hooks at var/lib/dracut/hooks/ instead of
usr/lib/dracut/hooks/. Rocky 9 dracut never found the hooks and dropped
to the emergency shell on all aarch64 nodes.

Use `cp -a el8 el10` as the x86_64 spec already does, so the rename
only affects the el10 copy.

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Timothy Middelkoop <tmiddelkoop@internet2.edu>
2026-04-30 08:09:06 -04:00
Jarrod Johnson 6d27e8a009 Allow monitor to read attributes by 'all' resource. 2026-04-29 07:51:08 -04:00
Jarrod Johnson f363796439 Write to stdout as binary
This allows better redirection.

In python3, must write to sys.stdout.buffer.  AttributeError for the unlikely event of a python2 based node being deployed.
2026-04-29 07:45:49 -04:00
Jarrod Johnson dec118a985 Fix mistake in spec file 2026-04-24 09:29:31 -04:00
Jarrod Johnson 38eb0d7b10 Add Ubuntu 26.04 2026-04-24 08:35:44 -04:00
xu_ren_xian ae338daa43 Handle confluent= boot arg and IPv4 NIC autodetect
Add support for a confluent=<host> kernel argument in init-premount: configure networking, flush interfaces, autodetect the primary NIC (saved to /tmp/autodetectnic), verify TLS connectivity to the provided server, call the whoami endpoint over TLS to obtain the node name, and write results to /custom-installation/confluent/confluent.info (with fallback to copernicus on failure).

Also update casper-bottom logic to handle IPv4 manager addresses: for IPv6 the manager is still bracketed and scoped interface resolved as before; for IPv4 the script now uses the previously detected NIC (/tmp/autodetectnic) or falls back to an `ip route get <mgr>` lookup to determine DEVICE. This ensures routed IPv4 deployments work correctly.
2026-04-23 17:50:41 -04:00
Jarrod Johnson 6ad3f0d70c Fix mistakes in the node apoption samples 2026-04-20 09:46:45 -04:00
Jarrod Johnson c0b9bb3ab1 Fix group rename in collective 2026-04-17 11:57:35 -04:00
Jarrod Johnson b32755b0d3 Fix remote client operation with Python 3.12+ 2026-04-17 09:01:12 -04:00
Jarrod Johnson c54ac530e1 Handle some environments where timedatectl does not exist 2026-04-14 13:50:12 -04:00
Jarrod Johnson 8990622470 Improve certificate mismatch handling 2026-04-08 15:37:50 -04:00
Jarrod Johnson 93a35d7e77 Improve srlinux error handling 2026-04-08 15:30:43 -04:00
Jarrod Johnson c49b2fd8ab Update quorum on deletion
If deletion of a node brings quorum, notify followers
of the good news
2026-04-07 14:57:09 -04:00
Jarrod Johnson 3ce2a5bc26 More tightly constrain node profile requests
Normalize paths using abspath and validate the result is within the expected path.
2026-04-06 15:12:44 -04:00
Jarrod Johnson 69d984b9dc Fix syntax mistake in deferred handling in nodeapply 2026-04-03 10:34:20 -04:00
Jarrod Johnson a123165712 Improve error when unknown user specified in syncfiles 2026-04-02 15:29:31 -04:00
Jarrod Johnson b91b10552c EL10 doesn't do setgid keysign
chmod 600 instead
2026-03-25 12:59:40 -04:00
Jarrod Johnson 779b07d2c2 Only try to use ssh_keys if it exists
EL10 changed from using ssh_keys and setgid to just
do setuid root instead.
2026-03-25 12:56:16 -04:00
Jarrod Johnson df73c14475 Support unconfigured good without space
Some platforms try to combine the words
2026-03-19 18:05:38 -04:00
Jarrod Johnson f78b301143 Update usage text 2026-03-19 09:54:08 -04:00
Jarrod Johnson 9b00fe5521 Don't try to open a file that doesn't exist 2026-03-17 13:03:18 -04:00
Jarrod Johnson 13a6444541 Fix incorrectly matching older versions as 'el10' 2026-03-17 12:58:04 -04:00
Jarrod Johnson 52db46be93 Fix python detection from ansible with space in shebang 2026-03-13 11:41:16 -04:00
Jarrod Johnson 550dfbf6a0 Fix reference of inputdata in remoteconfig 2026-03-13 09:26:11 -04:00
Jarrod Johnson e0951b11a6 Fix filename typo 2026-03-13 08:58:58 -04:00
Jarrod Johnson 1a87701fee Fix ansible running
Have results available as they happen

change away from stdout, to avoid being stepped on by ansible modules that print to that
2026-03-09 16:48:42 -04:00
Jarrod Johnson e185f2224f Implement ability for user to kick off confluent ansible runs
Add nodeapply -A and associated API.

This permits orchestrating plays without touching the nodes directly by the user.
2026-03-06 16:24:26 -05:00
Jarrod Johnson 69beaad3c9 Induce more versions of openssh to do the proper thing 2026-02-23 15:07:19 -05:00
Jarrod Johnson 74dda48513 Provide helper script for setting up nokia switches 2026-02-23 10:15:55 -05:00
Jarrod Johnson 08b2e1d008 Wire up FDB and LLDP for srlinux 2026-02-18 16:53:12 -05:00
Jarrod Johnson 582842aec8 Add mac and lldp retrieval for SRLinux 2026-02-18 16:16:22 -05:00
Jarrod Johnson 63307c331e Have nodesensors and nodehealth be more adaptive to partial server data. 2026-02-17 16:19:41 -05:00
Jarrod Johnson 318608cde3 Add draft SRLinux support
Wire up the non-networking facets of Nokia SR Linux support.

Provide stubs for LLDP and FDB
2026-02-17 16:13:43 -05:00
Jarrod Johnson ef7d2414ad Update nodeconfig usage material 2026-02-11 11:35:09 -05:00
Jarrod Johnson 722a0b874a Add notation about certificate and nodemedia 2026-02-11 10:54:30 -05:00
Jarrod Johnson 1deb76989e Recognize 1a/2b style enclosure bay in discovery 2026-02-10 17:10:18 -05:00
Jarrod Johnson 480d399f44 Add missing switch member of info with NX switches 2026-02-09 13:17:45 -05:00
Jarrod Johnson 07369667f7 Become incompatible with pysnmp 7.1.16
The EPEL version of pysnmp is broken, block it from dependecies
2026-02-06 15:13:46 -05:00
Jarrod Johnson e1d4b72f32 Be less picky about megarac url
megarac implementations consistently indicate an .xml file, but wildly vary on what it may be.

Broaden recognition.
2026-02-05 07:57:25 -05:00
Jarrod Johnson 86783a2f12 Fix uninitialized privacy_protocol variable 2026-02-03 07:58:07 -05:00
Jarrod Johnson 99063eb049 Recognize variation in DeviceDescrption.json to see SMM3 2026-02-02 10:17:32 -05:00
Jarrod Johnson 0975bd9e62 Revert "Update some code for async"
This reverts commit 3058dd4141.
2026-01-28 15:04:49 -05:00
Jarrod Johnson 3058dd4141 Update some code for async 2026-01-28 14:49:58 -05:00
Jarrod Johnson 21c9158491 Carry forward some dns attributes into a bond 2026-01-21 15:12:23 -05:00
Jarrod Johnson e6c19388a2 Add device-manager to container build
Confluent needs device-mapper for imgutil operation
2026-01-16 08:45:12 -05:00
Jarrod Johnson 048780e16d Explicitly mknodes for pack/unpack
In some contexts, udev may be asleep
at the wheel. Explictly have dmsetup
refresh the devnodes.
2026-01-15 15:15:11 -05:00
Jarrod Johnson 61d7a49163 Revert "Fallback to filename for PE format kernels"
This reverts commit a0a5887214.
2026-01-15 14:29:31 -05:00
Jarrod Johnson f8b8ce3847 Fallback to filename for PE format kernels
Some ARM64 kernels ship as EFI executables, but it's
not obvious how to extract version numbers from those properly.
2026-01-15 14:29:23 -05:00
Jarrod Johnson a0a5887214 Fallback to filename for PE format kernels
Some ARM64 kernels ship as EFI executables, but it's
not obvious how to extract version numbers from those properly.
2026-01-15 13:27:21 -05:00
Jarrod Johnson ccaf22f44f Add architecture handling in pkglist
To handle amd64/arm64 profiles, have the pkglist allow for architecture specific qualifiers.

Additionally, soften failure to accomplish selinux changes.
2026-01-15 12:52:07 -05:00
Jarrod Johnson 72c4868073 Update container with more packages, volumes, env, and alma 10 2026-01-15 09:46:23 -05:00
Jarrod Johnson afb6356f9d Change ownership
Container runs as internal 'root' user for now
2026-01-14 16:29:31 -05:00
Jarrod Johnson 6e6ac67b3d Provide some build assets
Provide some dockerfiles for creating build containers
2026-01-13 13:57:37 -05:00
Jarrod Johnson 99d10896e8 Fix parameter count unpack for accelerated switch interrogation 2026-01-08 17:07:39 -05:00
Jarrod Johnson 488f23e3ed Fix spelling of rpmbuild 2026-01-06 15:55:36 -05:00
Jarrod Johnson 6ca62cbb35 Provide optional output directory 2026-01-06 15:54:46 -05:00
Jarrod Johnson 45bc9788b4 Correct mistake in SPECS spelling 2026-01-06 15:51:40 -05:00
Jarrod Johnson 289c31e7ac Ensure in expected directory to start 2026-01-06 15:51:06 -05:00
Jarrod Johnson 1a684f2012 Ensure rpmbuild directory exists before building 2026-01-06 15:49:50 -05:00
Jarrod Johnson a4229fc58d Change name to index in apiclient
confignet was using the index for ipv4
2025-12-12 11:18:33 -05:00
Jarrod Johnson 31c1a865dc Update confignet to match apiclient changes 2025-12-12 09:30:56 -05:00
Jarrod Johnson ff84fcf6e9 Merge branch '3.14' 2025-12-11 13:21:33 -05:00
Jarrod Johnson 56dfb6dc6b Fix spelling issue in man page 2025-12-11 08:46:59 -05:00
Jarrod Johnson d7577a04a7 Fix ESXi compatibility of apiclient
apiclient was using Linux specific network  information.

Change to libc getifaddrs for better cross-platform compatibility.
2025-12-11 08:46:19 -05:00
Jarrod Johnson b72d6c9cfc Fix typo 2025-12-10 14:14:14 -05:00
Jarrod Johnson 523c93dfc3 Tolerate more network circumstances in bluefield deploy
If the networking didn't come up well, the 'functions' routines would not be able to handle.

Switch to using apiclient which is designed specifically to handle less cooperative
initial network conditions.
2025-12-09 08:49:27 -05:00
Jarrod Johnson 04e983a2d3 Handle broader memory information being returned from confluent 2025-12-04 09:52:15 -05:00
Jarrod Johnson 2464e0ff4f Fix location of the apiclient common resource 2025-12-02 14:35:50 -05:00
Jarrod Johnson c196bf9d55 Fix initial startup of a new confluent
The indexes change failed on a brand new install.
2025-12-02 14:31:10 -05:00
Jarrod Johnson 12d886a4f6 Add more imgutil documentation 2025-11-25 13:19:03 -05:00
Jarrod Johnson 6a26ece782 Merge remote-tracking branch 'xcat/master' 2025-11-25 11:59:43 -05:00
Jarrod Johnson 3cbac38d57 Also autoconsole when exactly one serial port is detected at all. 2025-11-25 11:53:50 -05:00
Jarrod Johnson 224f349053 Extend autocons to more use cases
If SPCR comes up blank, see if there is one and exactly one serial with carrier detect

Failing that, give DMI a chance to indicate a preference, for now just SuperMicro, since they have the most
inconsistent carrier detect behavior
but almost always consider ttyS1 to be the answer.
2025-11-25 11:51:07 -05:00
Jarrod Johnson 9d361d376d Merge pull request #203 from Obihoernchen/bond_desc
Add bond alias to team description
2025-11-21 09:47:09 -05:00
Markus Hilger ec39de3df0 Add bond alias to team description 2025-11-21 14:16:07 +01:00
Jarrod Johnson a3b768c70f Draft bluefield deploymeent facilities 2025-11-20 16:44:24 -05:00
Jarrod Johnson 4f75d4942b Modify adoption process:
Restore useinsecureprotocols if set directly on node

Switch from pxe-style to identity-file based node api token for hardened node authentication
2025-11-20 16:05:22 -05:00
Jarrod Johnson 4d2f36917c Restore useinsecureprotocols after adopt 2025-11-20 15:49:51 -05:00
Jarrod Johnson a2a50d34d1 Merge remote-tracking branch 'xcat' 2025-11-19 15:38:01 -05:00
Jarrod Johnson 041008a524 Remove redundant el10 initramfs fixup 2025-11-19 15:37:29 -05:00
Jarrod Johnson 5923feaa18 Merge pull request #202 from Obihoernchen/custom
Add documentation for custom nodeattribs
2025-11-19 07:47:46 -05:00
Jarrod Johnson 73216fc062 Fix architecture name mismatch
Confluent went with aarch64 consistent
with EL naming, but Ubuntu used
debian naming, recognize and just
handle that.
2025-11-18 09:10:30 -05:00
Jarrod Johnson 100944490c Fix potentially uninitialized curridx 2025-11-17 15:07:17 -05:00
Jarrod Johnson 61b07e0af4 Start index at 1 instead of 0 2025-11-17 12:05:03 -05:00
Jarrod Johnson 53760ab5dd Attribute feature enhancement
Add expression functions upper, lower, block_number, and block_offset.

Add an 'id.index' auto-attribute to
yield a number for nodes.
2025-11-17 11:58:04 -05:00
Jarrod Johnson d3e7a49f92 Simplify by recursion
Use _handle_ast_node to process
everything before the function name in an Attribute call
2025-11-15 10:32:11 -05:00
Jarrod Johnson 1f688ead28 Implement .replace() for attribute expressions
Provide an easy to use replace() to allow removing or substiting values
during expression evaluation.
2025-11-14 17:20:06 -05:00
Jarrod Johnson d20c5ac6eb Move handling of the loop directio straight to onboot
There were difficulties in the devfs after
boot, just let the full system handle it.
2025-11-13 15:33:04 -05:00
Jarrod Johnson 4484216198 Fix issues with the tethered memory optimizations 2025-11-13 15:24:26 -05:00
Jarrod Johnson e1efd6a9c5 Implement new 'uncompressed' image method
This allows the FS to just live, uncompressed, in cache.

This is generally a bad idea, however:

- In a hypothetically super-tuned diskless image, the lack of double-cache can offset the lack of compression
- The image will have supreme read performance
- It will have the most deterministic memory behavior
2025-11-13 14:39:53 -05:00
Jarrod Johnson 58d5209595 Port tethered improvments to EL8 2025-11-13 14:35:18 -05:00
Jarrod Johnson 53c918042a Remove double-caching in tethered diskless
By default, the squashfs file was being cached as well as the contents after extraction.

This is superfluous pressure on the cache of the OS.

However, it does help keep the image afloat through 'confignet', so
leave it on until onboot completes, then reclaim cache and disable further caching.
2025-11-13 14:28:25 -05:00
Markus Hilger 9148a841b5 Add documentation for custom nodeattribs 2025-11-13 00:45:53 +01:00
Jarrod Johnson 6ebb6de107 Allow specifiying SNMP privacy protocol
Modern SNMP devices may require AES.

Unfortunately, older ones may refuse AES.

For compatibility, continue to default to DES, but
allow AES to be indicated in attributes.
2025-11-10 10:21:01 -05:00
Jarrod Johnson 20292cdfd0 Do not let diskless.conf persist into EL9 diskless images
It fouls run of kdump building the kdump image.
2025-11-07 13:22:21 -05:00
Jarrod Johnson b07da455c2 Fix SAN generation
The nameconstraint support missed
a branch, fix this.
2025-11-07 11:22:12 -05:00
Jarrod Johnson cc9a81103b Do not autosign if the corresponding cryptography is unavailable
We use cryptography verification, but it's relatively new.

For compatibility, we fall back to fingerprint only.

This is pretty bad when inflicted on
unsuspecting users on autosign,
so skip autosign if cert validation
would break.
2025-11-04 15:51:22 -05:00
Jarrod Johnson 21155d2091 Bring untethered changes to el10 diskless 2025-11-04 11:17:28 -05:00
Jarrod Johnson 6c0d7ea60e Simplify end untethered el9 diskless environment
Rather than treat both as the same, since untethered has everything up front anyway, go ahead and extract the filesystem.

This makes the mount look more straightforward and makes it so deletion of files from
the image also frees ram.
2025-11-04 11:14:52 -05:00
Jarrod Johnson 174d204607 Implement compatibility with newer pysnmp
For now, terminate the async nature
if newer pysnmp is detected.
2025-11-04 09:58:11 -05:00
Jarrod Johnson 2826abb7ab Prune excessive leftover ext config files 2025-11-03 14:21:36 -05:00
Jarrod Johnson 5adb5fa780 Automatically sign XCC certificates on discover
If an XCC doesn't have a 'real' certificate, sign it with the confluent
CA for 47 days.
2025-11-03 14:02:33 -05:00
Jarrod Johnson 5de063212f Prepare for supporting constrained CA
If asked to sign using a name constrained CA,
avoid generating a certificate that
would violate those constraints.
2025-11-03 10:43:34 -05:00
Jarrod Johnson 073f6d1389 Wire up cert signing to nodecertutil 2025-10-31 12:04:27 -04:00
Jarrod Johnson f755ba9f91 Implement method to sign BMC certificates 2025-10-31 10:46:42 -04:00
Jarrod Johnson cf8c01ef13 Merge remote-tracking branch 'lenovo' 2025-10-31 09:48:05 -04:00
Jarrod Johnson 8b12047ae0 Update to handle newer XCC2 firmware 2025-10-31 09:45:59 -04:00
Jarrod Johnson f0a779764d Fix ordering of digest argument
The digest argument was erroneously inserted between startdate and it's
argument, correct this mistake.
2025-10-28 15:39:04 -04:00
Jarrod Johnson 0ad7e99efe Only optionally use cryptography verification
Some supported distributions can't run the newer cryptography.

Make it a feature that only works with newer platforms.
2025-10-27 08:38:14 -04:00
Jarrod Johnson 24a76612ae Use sha284 hash algorithm
Some implementations reject sha256 as inadequate if ecdsa has 384 bit keylength. Bring the digest up to match
the key size for the ECDSA.
2025-10-27 06:41:05 -04:00
Jarrod Johnson 6c9c58f464 Update certutil to prepare for broader usage
For one, apply more rules from CA/B forum. This includes including KU and EKU extensions, marking basicConstraints critical, and
randomized serial numbers.

Also make the backdate and end date configurable, to allow
for the BMC certs to have a more palatable validity interval.
2025-10-26 14:57:26 -04:00
Jarrod Johnson 3125f4171b Begin overhaul of TLS cert management
Begin expanding certutil to sign other certificates from external CSRs more easily.

Have certutil make the CA constraint critical.

Have the fingerprint based validator have a mechanism to check for properly signed certificate in lieu of exact match,
and update the stored fingerprint
on match.

Provide a means to request a custom subject when evaluating a
target.

Change redfish plugin to set that subject in the verifier.
2025-10-24 20:02:51 -04:00
95 changed files with 2817 additions and 382 deletions
+8
View File
@@ -0,0 +1,8 @@
FROM almalinux:10
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "git", "golang"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
+6
View File
@@ -0,0 +1,6 @@
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
+3
View File
@@ -0,0 +1,3 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
+8
View File
@@ -0,0 +1,8 @@
FROM almalinux:8
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "rpm-sign", "git", "fuse-devel","libcurl-devel"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
+2
View File
@@ -0,0 +1,2 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
+10
View File
@@ -0,0 +1,10 @@
FROM almalinux:9
RUN ["yum", "-y","update"]
RUN ["yum", "-y","install","gcc","make","rpm-build","python3-devel","python3-setuptools","createrepo","python3", "perl", "perl-DBI", "perl-JSON", "perl-Net-DNS", "perl-DB_File", "perl-XML-LibXML", "pinentry-tty", "rpm-sign", "epel-release", "git"]
RUN ["crb", "enable"]
RUN ["yum", "-y","install","fuse-devel","libcurl-devel"]
ADD rpmmacro /root/.rpmmacros
ADD buildpackages.sh /bin/
#VOLUME ["/rpms", "/srpms"]
CMD ["/bin/bash","/bin/buildpackages.sh"]
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
for package in /srpms/*; do
rpmbuild --rebuild $package
done
find ~/rpmbuild/RPMS -type f -exec cp {} /rpms/ \;
+2
View File
@@ -0,0 +1,2 @@
%_gpg_digest_algo sha256
%_gpg_name Lenovo Scalable Infrastructure
+12
View File
@@ -0,0 +1,12 @@
FROM ubuntu:noble
ADD stdeb.patch /tmp/
ADD buildapt.sh /bin/
ADD distributions.tmpl /bin/
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "reprepro", "python3-stdeb", "gnupg-agent", "devscripts", "debhelper", "libsoap-lite-perl", "libdbi-perl", "quilt", "git", "python3-pyparsing", "python3-dnspython", "python3-eventlet", "python3-netifaces", "python3-paramiko", "dh-python", "libjson-perl", "ronn", "alien", "gcc", "make"]
RUN ["mkdir", "-p", "/sources/git/"]
RUN ["mkdir", "-p", "/debs/"]
RUN ["mkdir", "-p", "/apt/"]
RUN ["bash", "-c", "patch -p1 < /tmp/stdeb.patch"]
CMD ["/bin/bash", "/bin/buildapt.sh"]
+21
View File
@@ -0,0 +1,21 @@
#cp -a /sources/git /tmp
for builder in $(find /sources/git -name builddeb); do
cd $(dirname $builder)
./builddeb /debs/
done
cp /prebuilt/* /debs/
cp /osd/*.deb /debs/
mkdir -p /apt/conf/
CODENAME=$(grep VERSION_CODENAME= /etc/os-release | sed -e 's/.*=//')
if [ -z "$CODENAME" ]; then
CODENAME=$(grep VERSION= /etc/os-release | sed -e 's/.*(//' -e 's/).*//')
fi
if ! grep $CODENAME /apt/conf/distributions; then
sed -e s/#CODENAME#/$CODENAME/ /bin/distributions.tmpl >> /apt/conf/distributions
fi
cd /apt/
reprepro includedeb $CODENAME /debs/*.deb
for dsc in /debs/*.dsc; do
reprepro includedsc $CODENAME $dsc
done
+7
View File
@@ -0,0 +1,7 @@
Origin: Lenovo HPC Packages
Label: Lenovo HPC Packages
Codename: #CODENAME#
Architectures: amd64 source
Components: main
Description: Lenovo HPC Packages
+34
View File
@@ -0,0 +1,34 @@
diff -urN t/usr/lib/python3/dist-packages/stdeb/cli_runner.py t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py
--- t/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:30:13.930328999 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/cli_runner.py 2024-06-11 18:32:05.392731405 +0000
@@ -8,7 +8,7 @@
from ConfigParser import SafeConfigParser # noqa: F401
except ImportError:
# python 3.x
- from configparser import SafeConfigParser # noqa: F401
+ from configparser import ConfigParser # noqa: F401
from distutils.util import strtobool
from distutils.fancy_getopt import FancyGetopt, translate_longopt
from stdeb.util import stdeb_cmdline_opts, stdeb_cmd_bool_opts
diff -urN t/usr/lib/python3/dist-packages/stdeb/util.py t.patch/usr/lib/python3/dist-packages/stdeb/util.py
--- t/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:32:53.864776149 +0000
+++ t.patch/usr/lib/python3/dist-packages/stdeb/util.py 2024-06-11 18:33:02.063952870 +0000
@@ -730,7 +730,7 @@
example.
"""
- cfg = ConfigParser.SafeConfigParser()
+ cfg = ConfigParser.ConfigParser()
cfg.read(cfg_files)
if cfg.has_section(module_name):
section_items = cfg.items(module_name)
@@ -801,7 +801,7 @@
if len(cfg_files):
check_cfg_files(cfg_files, module_name)
- cfg = ConfigParser.SafeConfigParser(cfg_defaults)
+ cfg = ConfigParser.ConfigParser(cfg_defaults)
for cfg_file in cfg_files:
with codecs.open(cfg_file, mode='r', encoding='utf-8') as fd:
cfg.readfp(fd)
+9
View File
@@ -0,0 +1,9 @@
cd ~/confluent
git pull
rm ~/rpmbuild/RPMS/noarch/*osdeploy*
rm ~/rpmbuild/SRPMS/*osdeploy*
sh confluent_osdeploy/buildrpm-aarch64
mkdir -p $HOME/el9/
mkdir -p $HOME/el10/
podman run --rm -it -v $HOME:/build el9build bash /build/confluent/confluent_vtbufferd/buildrpm /build/el9/
+160 -7
View File
@@ -133,6 +133,8 @@ def print_help():
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
if opts.headless:
return
status = consolename
info = []
for statekey in stateinfo:
@@ -455,8 +457,10 @@ def do_command(command, server):
currconsole = targpath
startrequest = {'operation': 'start', 'path': targpath,
'parameters': {}}
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
height, width = 31, 100
if not opts.headless:
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
startrequest['parameters']['width'] = width
startrequest['parameters']['height'] = height
for param in argv[2:]:
@@ -624,17 +628,19 @@ def startconsole(nodename):
signal.signal(signal.SIGWINCH, do_resize)
didconsole = True
consolename = nodename
tty.setraw(sys.stdin.fileno())
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
if not opts.headless:
tty.setraw(sys.stdin.fileno())
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
inconsole = True
check_automation('') # give any leading 'sends' a chance
def quitconfetty(code=0, fullexit=False, fixterm=True):
global inconsole
global currconsole
global didconsole
if fixterm or didconsole:
if (fixterm or didconsole) and not opts.headless:
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl & ~os.O_NONBLOCK)
if oldtcattr is not None:
@@ -844,6 +850,20 @@ def check_escape_seq(currinput, filehandle):
currinput += filehandle.read()
return currinput
automation_directives = []
current_automation_directive = None
automation_map = {
'<up>': '\x1b[A',
'<down>': '\x1b[B',
'<right>': '\x1b[C',
'<left>': '\x1b[D',
'<enter>': '\r',
'<esc>': '\x1b',
'<tab>': '\t',
}
parser = optparse.OptionParser()
parser.add_option("-s", "--server", dest="netserver",
help="Confluent instance to connect to",
@@ -851,12 +871,64 @@ parser.add_option("-s", "--server", dest="netserver",
parser.add_option("-c", "--control", dest="controlpath",
help="Path to offer terminal control",
metavar="PATH")
parser.add_option('-a', '--automation', type='string', default=None,
help='Specify an automation script to run', metavar='SCRIPT')
parser.add_option('-e', '--headless', action='store_true', default=False,
help='Run in headless mode, which is designed for use with '
'automation scripts and disables interactive features')
parser.add_option(
'-m', '--mintime', default=0,
help='Minimum time to run or else pause for input (used to keep a '
'terminal from closing quickly on error)')
opts, shellargs = parser.parse_args()
def parse_automation_script(script, session_node):
global current_automation_directive
for line in script.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('exit'):
automation_directives.append(('exit', None))
continue
if ' ' not in line:
sys.stderr.write("Invalid line in automation script: %s\n" % line)
continue
directive, arg = line.split(' ', 1)
directive = directive.strip().lower()
if directive not in ('expect', 'send', 'forget'):
sys.stderr.write("Unknown directive in automation script: %s\n" % directive)
continue
arg = arg.strip()
origarg = arg
if arg[0] not in ('"', "'"):
arg = '"' + arg + '"'
if arg[0] == "'" and arg[-1] == "'":
# do not process '<>' sequences in single quotes
arg = arg[1:-1]
arg = bytes(arg, "utf-8").decode("unicode_escape")
elif arg[0] == '"' and arg[-1] == '"':
arg = bytes(arg[1:-1], "utf-8").decode("unicode_escape")
for key, value in automation_map.items():
arg = arg.replace(key, value)
arg = re.sub(r'<env:(\w+)>', lambda m: os.environ[m.group(1)], arg)
if '{' in arg: # support confluent expressions
for res in session.create('/nodes/{0}/attributes/expression'.format(session_node),
{'expression': arg}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
sys.exit(1)
if 'value' in res:
arg = res['value']
automation_directives.append((directive, arg, origarg))
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
username = None
passphrase = None
def server_connect():
@@ -909,6 +981,7 @@ def main():
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
global powerstate, powertime, clearpowermessage
if sys.stdout.isatty():
@@ -925,6 +998,9 @@ def main():
targ, session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
if opts.automation:
with open(opts.automation) as f:
parse_automation_script(f.read(), session_node)
do_command("start %s" % targ, netserver)
doexit = True
elif shellargs:
@@ -936,9 +1012,13 @@ def main():
while inconsole or not doexit:
if inconsole:
if opts.headless:
handles = [session.connection]
else:
handles = (sys.stdin, session.connection)
try:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 10)
handles, (), (), 10)
except select.error:
rdylist = ()
for fh in rdylist:
@@ -976,6 +1056,72 @@ fgcolor = None
bgcolor = None
fgshifted = False
pendseq = ''
automation_check = ''
def check_automation(data):
global automation_check
global current_automation_directive
if type(data) != str:
data = data.decode('utf-8', errors='ignore')
while current_automation_directive:
if current_automation_directive[0] == 'forget' and data:
current_automation_directive = None
automation_check = ''
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\n')
sys.stdout.flush()
return
if current_automation_directive[0] == 'expect':
expected = current_automation_directive[1]
combined = automation_check + data
if expected and expected in combined:
data = data[combined.rindex(expected) + len(expected):]
if opts.headless:
sys.stdout.write(f'Detected {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
current_automation_directive = None
automation_check = ''
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
else:
# Check if there's potential start of expected data in the incoming data
combined = automation_check + data
automation_check = ''
for i in range(1, min(len(expected), len(combined)) + 1):
if expected.startswith(combined[-i:]):
automation_check = combined[-i:]
return # wait for next check
elif current_automation_directive[0] == 'send':
data = ''
automation_check = ''
if opts.headless:
sys.stdout.write(f'Sending {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
if current_automation_directive[1]:
tlvdata.send(session.connection, current_automation_directive[1])
current_automation_directive = None
if automation_directives:
current_automation_directive = automation_directives.pop(0)
if opts.headless and current_automation_directive[0] == 'expect':
sys.stdout.write(f'Expecting {repr(current_automation_directive[2])}\r\n')
sys.stdout.flush()
elif current_automation_directive[0] == 'exit':
if opts.headless:
sys.stdout.write('Automation completed\r\n')
sys.stdout.flush()
return True
if opts.headless and not current_automation_directive:
sys.stdout.write('Automation completed\r\n')
sys.stdout.flush()
return True
return False
def consume_termdata(fh, bufferonly=False):
global clearpowermessage
global fgcolor, bgcolor, fgshifted, pendseq
@@ -987,6 +1133,11 @@ def consume_termdata(fh, bufferonly=False):
updatestatus(data)
return ''
if data is not None:
shouldexit = check_automation(data)
if opts.headless:
if shouldexit:
quitconfetty(fullexit=True)
return ''
indata = pendseq + client.stringify(data)
pendseq = ''
data = ''
@@ -1066,6 +1217,8 @@ def consume_termdata(fh, bufferonly=False):
# this scenario comfortable that it
# will come out soon enough
pass
if shouldexit:
quitconfetty(fullexit=True)
else:
deadline = 5
connected = False
+46 -8
View File
@@ -36,6 +36,36 @@ import confluent.client as client
import confluent.sortutil as sortutil
devnull = None
def run_automation(noderange, category, c):
automationbynode = {}
for res in c.update('/noderange/{0}/deployment/remote_config/run'.format(noderange), {
'category': category,
}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
if 'created' in res:
nodename = res['created'].split('/')[2]
automationbynode[nodename] = res['created']
while automationbynode:
for node in list(automationbynode):
for res in c.read(automationbynode[node]):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
for result in res.get('results', []):
sys.stdout.write('{0}: Task [{1}] {2}\n'.format(
node, result['task_name'], result['state']))
for warning in result.get('warnings', []):
sys.stderr.write('{0}: [WARNING] {1}\n'.format(node, warning))
if 'errorinfo' in result:
for errorline in result['errorinfo'].splitlines():
sys.stderr.write('{0}: [ERROR] {1}\n'.format(node, errorline))
if res.get('complete', False):
del automationbynode[node]
sys.stdout.write('{0}: Automation complete\n'.format(node))
def run():
global devnull
devnull = open(os.devnull, 'rb')
@@ -51,6 +81,8 @@ def run():
help='Run the syncfiles associated with the currently completed OS profile on the noderange')
argparser.add_option('-P', '--scripts',
help='Re-run specified scripts, with full path under scripts, e.g. post.d/first,firstboot.d/second')
argparser.add_option('-A', '--automation',
help='Run the automation scripts associated with the current OS profile on the noderange, specifying category (onboot.d/firstboot.d/post.d)')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run remote ssh command to, '
@@ -74,16 +106,13 @@ def run():
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
if options.automation:
run_automation(args[0], options.automation, c)
nodemap = {}
cmdparms = []
nodes = []
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
nodes.append(node)
cmdstorun = []
if options.security:
@@ -94,8 +123,17 @@ def run():
for script in options.scripts.split(','):
cmdstorun.append(['run_remote', script])
if not cmdstorun:
if options.automation:
sys.exit(0)
argparser.print_help()
sys.exit(1)
for res in c.read('/noderange/{0}/nodes/'.format(args[0])):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode |= res.get('errorcode', 1)
break
node = res['item']['href'][:-1]
nodes.append(node)
idxbynode = {}
cmdvbase = ['bash', '/etc/confluent/functions']
for sshnode in nodes:
@@ -145,7 +183,7 @@ def run():
run_cmdv(node, cmdv, all, poller, pipedesc)
elif pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller. pipedesc)
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
+15
View File
@@ -76,6 +76,10 @@ if __name__ == '__main__':
list_parser = subparsers.add_parser('listbmccacerts', help='List BMC CA certificates')
sign_bmc_parser = subparsers.add_parser('signbmccert', help='Sign BMC certificate')
sign_bmc_parser.add_argument('--days', type=int, help='Number of days the certificate is valid for')
sign_bmc_parser.add_argument('--added-names', type=str, help='Additional names to include in the certificate')
args = parser.parse_args()
c = client.Command()
if args.command == 'installbmccacert':
@@ -84,6 +88,17 @@ if __name__ == '__main__':
removebmccacert(args.noderange, args.id, c)
elif args.command == 'listbmccacerts':
listbmccacerts(args.noderange, c)
elif args.command == 'signbmccert':
payload = {}
if args.days is not None:
payload['days'] = args.days
else:
print("Error: --days is required for signbmccert", file=sys.stderr)
sys.exit(1)
if args.added_names:
payload['added_names'] = args.added_names
for res in c.update(f'/noderange/{args.noderange}/configuration/management_controller/certificate/sign', payload):
print(repr(res))
else:
parser.print_help()
sys.exit(1)
+2 -2
View File
@@ -62,7 +62,7 @@ argparser.add_option('-e', '--extra', dest='extra',
'to be extra configuration')
argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
help='Treat named settings as items to not '
'examine, compare, or restore default')
argparser.add_option('-a', '--advanced', dest='advanced',
action='store_true', default=False,
@@ -73,7 +73,7 @@ argparser.add_option('-r', '--restoredefault', default=False,
dest='restoredefault', metavar="COMPONENT",
help='Restore the configuration of the node '
'to factory default for given component. '
'Currently only uefi is supported')
'Currently "uefi" and "bmc" are supported components')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to configure, '
+21 -8
View File
@@ -18,6 +18,7 @@
import base64
import optparse
import os
import shlex
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -62,8 +63,13 @@ argparser = optparse.OptionParser(
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
"For example, ctrl-'e', then 'c', then '.' will exit the current "
"console")
argparser.add_option('-a', '--automation', type='string', default=None,
help='Specify an automation script')
argparser.add_option('-t', '--tile', action='store_true', default=False,
help='Tile console windows in the terminal')
argparser.add_option('-e', '--headless', action='store_true', default=False,
help='Run in headless mode, which is designed for use with '
'automation scripts and disables interactive features')
argparser.add_option('-l', '--log', action='store_true', default=False,
help='Enter log replay mode instead of showing a live console')
@@ -98,6 +104,12 @@ argparser.add_option('-w','--windowed', action='store_true', default=False,
(options, args) = argparser.parse_args()
automation_args = []
if options.automation:
automation_args = ['-a', options.automation]
if options.headless:
automation_args += ['--headless']
oldtcattr = None
oldfl = None
@@ -655,7 +667,7 @@ if options.windowed:
firstnode=nodes[0]
nodes.pop(0)
with open(os.devnull, 'wb') as devnull:
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(firstnode), '-m', '5', 'start', '/nodes/{0}/console/session'.format(firstnode) ] , stdin=devnull)
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(firstnode)] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(firstnode)] , stdin=devnull)
time.sleep(2)
s=socket.socket(socket.AF_UNIX)
winid=''
@@ -727,7 +739,7 @@ if options.windowed:
else:
pass
with open(os.devnull, 'wb') as devnull:
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(node), '-m', '5', 'start', '/nodes/{0}/console/session'.format(node)] , stdin=devnull)
xopen=subprocess.Popen(envlist + [confettypath, '-c', '/tmp/controlpath-{0}'.format(node)] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)] , stdin=devnull)
sys.exit(0)
#end of wcons
if options.tile:
@@ -752,18 +764,18 @@ if options.tile:
panename = '{0}:{1}'.format(sessname, pane)
if initial:
initial = False
confetty_cmd = [confettypath] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)]
subprocess.call(
['tmux', 'new-session', '-d', '-s',
sessname, '-x', '800', '-y',
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
'800', ' '.join(shlex.quote(arg) for arg in confetty_cmd)])
else:
subprocess.call(['tmux', 'select-pane', '-t', sessname])
subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
confetty_cmd = [confettypath] + automation_args + ['-m', '5', 'start', '/nodes/{0}/console/session'.format(node)]
subprocess.call(
['tmux', 'split', '-h', '-t', sessname,
'{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
' '.join(shlex.quote(arg) for arg in confetty_cmd)])
subprocess.call(['tmux', 'select-layout', '-t', sessname, 'tiled'], stdout=null)
pane += 1
subprocess.call(['tmux', 'select-pane', '-t', sessname])
@@ -771,5 +783,6 @@ if options.tile:
if not in_tmux:
os.execlp('tmux', 'tmux', 'attach', '-t', sessname)
else:
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
execl_args = [confettypath] + automation_args + ['start',
'/nodes/{0}/console/session'.format(args[0])]
os.execl(confettypath, *execl_args)
+4 -4
View File
@@ -84,13 +84,13 @@ def main():
healthexplanations[node] = []
for sensor in health[node]['sensors']:
explanation = sensor['name'] + ':'
if sensor['value'] is not None:
if sensor.get('value', None) is not None:
explanation += str(sensor['value'])
if sensor['units'] is not None:
if sensor.get('units', None) is not None:
explanation += sensor['units']
if sensor['states']:
if sensor.get('states', None):
explanation += ','
if sensor['states']:
if sensor.get('states', None):
explanation += ','.join(sensor['states'])
healthexplanations[node].append(explanation)
if node in healthbynode and node in healthexplanations:
+5 -5
View File
@@ -123,7 +123,7 @@ def sensorpass(showout=True, appendtime=False):
if 'sensors' not in reading[node]:
continue
for sensedata in reading[node]['sensors']:
if sensedata['value'] is None and options.skipnumberless:
if sensedata.get('value', None) is None and options.skipnumberless:
continue
for redundant_state in ('Non-Critical', 'Critical'):
try:
@@ -134,17 +134,17 @@ def sensorpass(showout=True, appendtime=False):
resultdata[node][sensedata['name']] = sensedata
sensorname = sensedata['name']
sensorheaders[sensorname] = sensorname
if sensedata['units'] not in (None, u''):
if sensedata.get('units', None) not in (None, u''):
sensorheaders[sensorname] += u' ({0})'.format(
sensedata['units'])
if showout:
if sensedata['value'] is None:
if sensedata.get('value', None) is None:
showval = ''
elif isinstance(sensedata['value'], float):
showval = u' {0} '.format(floatformat(sensedata['value']))
else:
showval = u' {0} '.format(sensedata['value'])
if sensedata['units'] not in (None, u''):
showval = u' {0} '.format(sensedata.get('value', ''))
if sensedata.get('units', None) not in (None, u''):
showval += sensedata['units']
if sensedata.get('health', 'ok') != 'ok':
datadescription = [sensedata['health']]
+7 -2
View File
@@ -391,8 +391,13 @@ class Command(object):
cacert = None
certreqs = ssl.CERT_NONE
knownhosts = True
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
cert_reqs=certreqs)
tlsctx = ssl.create_default_context()
if certreqs == ssl.CERT_NONE:
tlsctx.check_hostname = False
tlsctx.verify_mode = certreqs
if cacert:
tlsctx.load_verify_locations(cacert)
self.connection = tlsctx.wrap_socket(self.connection, server_hostname=server)
if knownhosts:
certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
@@ -24,6 +24,8 @@ For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null value.
This is different from setting the value to an empty string.
Arbitrary custom attributes can also be created with the `custom.` prefix.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
+1 -2
View File
@@ -49,8 +49,7 @@ actually be in effect until a reboot.
* `-r COMPONENT`, `--restoredefault=COMPONENT`:
Request that the specified component of the targeted nodes will have its
configuration reset to default. Currently the only component implemented
is uefi.
configuration reset to default. The component may be "uefi" or "bmc".
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
Specify a maximum number of nodes to configure, prompting if over
@@ -11,7 +11,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
## DESCRIPTION
`nodegroupattrip` queries the confluent server to get information about nodes.
`nodegroupattrib` queries the confluent server to get information about nodes.
In the simplest form, it simply takes the given group and lists the attributes of that group.
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
+7
View File
@@ -32,6 +32,13 @@ BMCs map a virtual USB device to that url. Content is loaded on demand, and
as such that URL is referenced potentially once for every IO operation that
the host platform attempts.
## NOTES
When doing an attach of an https:// url, you may hit an error if you have not enrolled your certificate authority.
In a general confluent environment, you can usually address it by:
`# for cert in /var/lib/confluent/public/site/tls/*.pem; do nodecertutil s1-s4 installbmccacert $cert; done`
## OPTIONS
* `-h`, `--help`:
@@ -0,0 +1,74 @@
#!/usr/bin/python3
import glob
import gzip
import base64
import os
import subprocess
import sys
import tempfile
def collect_certificates(tmpdir):
certdata = ''
for cacert in glob.glob(f'{tmpdir}/*.pem'):
with open(cacert, 'r') as f:
certdata += f.read()
return certdata
def embed_certificates(incfg, certdata):
if not certdata:
raise Exception('No certificates found to embed')
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
return incfg
def embed_identity(incfg, identityjson):
incfg = incfg.replace('%IDENTJSON%', identityjson)
return incfg
def embed_apiclient(incfg, apiclient):
with open(apiclient, 'r') as f:
apiclientdata = f.read()
compressed = gzip.compress(apiclientdata.encode())
encoded = base64.b64encode(compressed).decode()
incfg = incfg.replace('%APICLIENTZ64%', encoded)
return incfg
def embed_data(tmpdir, outfile):
templatefile = f'{tmpdir}/bfb.cfg.template'
with open(templatefile, 'r') as f:
incfg = f.read()
certdata = collect_certificates(tmpdir)
incfg = embed_certificates(incfg, certdata)
with open(f'{tmpdir}/identity.json', 'r') as f:
identityjson = f.read()
incfg = embed_identity(incfg, identityjson)
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
with open(outfile, 'w') as f:
f.write(incfg)
def get_identity_json(node):
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
try:
with open(identity_file, 'r') as f:
return f.read()
except FileNotFoundError:
return None
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
sys.exit(1)
node = sys.argv[1]
bfbfile = sys.argv[2]
rshim = sys.argv[3]
os.chdir(os.path.dirname(os.path.abspath(__file__)))
currdir = os.getcwd()
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
@@ -0,0 +1,74 @@
#!/usr/bin/python3
import glob
import gzip
import base64
import os
import subprocess
import sys
import tempfile
def collect_certificates(tmpdir):
certdata = ''
for cacert in glob.glob(f'{tmpdir}/*.pem'):
with open(cacert, 'r') as f:
certdata += f.read()
return certdata
def embed_certificates(incfg, certdata):
if not certdata:
raise Exception('No certificates found to embed')
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
return incfg
def embed_identity(incfg, identityjson):
incfg = incfg.replace('%IDENTJSON%', identityjson)
return incfg
def embed_apiclient(incfg, apiclient):
with open(apiclient, 'r') as f:
apiclientdata = f.read()
compressed = gzip.compress(apiclientdata.encode())
encoded = base64.b64encode(compressed).decode()
incfg = incfg.replace('%APICLIENTZ64%', encoded)
return incfg
def embed_data(tmpdir, outfile):
templatefile = f'{tmpdir}/bfb.cfg.template'
with open(templatefile, 'r') as f:
incfg = f.read()
certdata = collect_certificates(tmpdir)
incfg = embed_certificates(incfg, certdata)
with open(f'{tmpdir}/identity.json', 'r') as f:
identityjson = f.read()
incfg = embed_identity(incfg, identityjson)
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
with open(outfile, 'w') as f:
f.write(incfg)
def get_identity_json(node):
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
try:
with open(identity_file, 'r') as f:
return f.read()
except FileNotFoundError:
return None
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
sys.exit(1)
node = sys.argv[1]
bfbfile = sys.argv[2]
rshim = sys.argv[3]
os.chdir(os.path.dirname(os.path.abspath(__file__)))
currdir = os.getcwd()
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
@@ -0,0 +1,76 @@
function bfb_modify_os() {
echo 'ubuntu:!' | chpasswd -e
mkdir -p /mnt/opt/confluent/bin/
cat > /mnt/opt/confluent/bin/confluentbootstrap.sh << 'END_OF_EMBED'
#!/bin/bash
cat > /usr/local/share/ca-certificates/confluent.crt << 'END_OF_CERTS'
%CONFLUENTCERTCOLL%
END_OF_CERTS
update-ca-certificates
mkdir -p /opt/confluent/bin /etc/confluent/
cp /usr/local/share/ca-certificates/confluent.crt /etc/confluent/ca.pem
cat > /opt/confluent/bin/apiclient.gz.b64 << 'END_OF_CLIENT'
%APICLIENTZ64%
END_OF_CLIENT
base64 -d /opt/confluent/bin/apiclient.gz.b64 | gunzip > /opt/confluent/bin/apiclient
cat > /etc/confluent/ident.json << 'END_OF_IDENT'
%IDENTJSON%
END_OF_IDENT
python3 /opt/confluent/bin/apiclient -i /etc/confluent/ident.json /confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
PROFILE=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
ROOTPASS=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg | awk '{print $2}'|grep -v null)
if [ -n "$ROOTPASS" ]; then
echo root:$ROOTPASS | chpasswd -e
echo "ubuntu:$ROOTPASS" | chpasswd -e
else
echo 'ubuntu:!' | chpasswd -e
fi
cntmp=$(mktemp -d)
cd "$cntmp" || { echo "Failed to cd to temporary directory $cntmp"; exit 1; }
touch /etc/confluent/confluent.deploycfg
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/confignet > confignet
python3 confignet
cd -
rm -rf "$cntmp"
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/functions > /etc/confluent/functions
bash /etc/confluent/functions run_remote setupssh
for cert in /etc/ssh/ssh*-cert.pub; do
if [ -s $cert ]; then
echo HostCertificate $cert >> /etc/ssh/sshd_config.d/90-confluent.conf
fi
done
mkdir -p /var/log/confluent
chmod 700 /var/log/confluent
touch /var/log/confluent/confluent-firstboot.log
touch /var/log/confluent/confluent-post.log
chmod 600 /var/log/confluent/confluent-post.log
chmod 600 /var/log/confluent/confluent-firstboot.log
exec >> /var/log/confluent/confluent-post.log
exec 2>> /var/log/confluent/confluent-post.log
bash /etc/confluent/functions run_remote_python syncfileclient
bash /etc/confluent/functions run_remote_parts post.d
bash /etc/confluent/functions run_remote_config post.d
exec >> /var/log/confluent/confluent-firstboot.log
exec 2>> /var/log/confluent/confluent-firstboot.log
bash /etc/confluent/functions run_remote_parts firstboot.d
bash /etc/confluent/functions run_remote_config firstboot.d
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: staged'
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: complete'
systemctl disable confluentbootstrap
rm /etc/systemd/system/confluentbootstrap.service
END_OF_EMBED
chmod +x /mnt/opt/confluent/bin/confluentbootstrap.sh
cat > /mnt/etc/systemd/system/confluentbootstrap.service << EOS
[Unit]
Description=First Boot Process
Requires=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/confluent/bin/confluentbootstrap.sh
[Install]
WantedBy=multi-user.target
EOS
chroot /mnt systemctl enable confluentbootstrap
}
@@ -0,0 +1,125 @@
#!/usr/bin/python3
import os
import sys
import tempfile
import glob
import shutil
import shlex
import subprocess
import select
sys.path.append('/opt/lib/confluent/python')
import confluent.sortutil as sortutil
import confluent.client as client
def prep_outdir(node):
tmpdir = tempfile.mkdtemp()
for certfile in glob.glob('/var/lib/confluent/public/site/tls/*.pem'):
basename = os.path.basename(certfile)
destfile = os.path.join(tmpdir, basename)
shutil.copy2(certfile, destfile)
subprocess.check_call(shlex.split(f'confetty set /nodes/{node}/deployment/ident_image=create'))
shutil.copy2(f'/var/lib/confluent/private/identity_files/{node}.json', os.path.join(tmpdir, 'identity.json'))
return tmpdir
def exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller):
remotedir = subprocess.check_output(shlex.split(f'ssh {host} mktemp -d /tmp/bfb.XXXXXX')).decode().strip()
bfbbasename = os.path.basename(bfbfile)
subprocess.check_call(shlex.split(f'rsync -avz --info=progress2 {bfbfile} {host}:{remotedir}/{bfbbasename}'))
subprocess.check_call(shlex.split(f'rsync -avc --info=progress2 /opt/lib/confluent/osdeploy/bluefield/hostscripts/ {host}:{remotedir}/'))
for node in nodetorshim:
rshim = nodetorshim[node]
nodeoutdir = prep_outdir(node)
nodeprofile = subprocess.check_output(shlex.split(f'nodeattrib {node} deployment.pendingprofile')).decode().strip().split(':', 2)[2].strip()
shutil.copy2(f'/var/lib/confluent/public/os/{nodeprofile}/bfb.cfg.template', os.path.join(nodeoutdir, 'bfb.cfg.template'))
subprocess.check_call(shlex.split(f'rsync -avz {nodeoutdir}/ {host}:{remotedir}/{node}/'))
shutil.rmtree(nodeoutdir)
run_cmdv(node, shlex.split(f'ssh {host} sh /etc/confluent/functions confluentpython {remotedir}/bfb-autoinstall {node} {remotedir}/{bfbbasename} {rshim}'), all, poller, pipedesc)
def run_cmdv(node, cmdv, all, poller, pipedesc):
try:
nopen = subprocess.Popen(
cmdv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError as e:
if e.errno == 2:
sys.stderr.write('{0}: Unable to find local executable file "{1}"\n'.format(node, cmdv[0]))
return
raise
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
all.add(nopen.stdout)
poller.register(nopen.stdout, select.EPOLLIN)
all.add(nopen.stderr)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
if len(sys.argv) < 3:
print(f'Usage: {sys.argv[0]} <host> <bfbfile> <node1:rshim1> [<node2:rshim2> ...]')
sys.exit(1)
host = sys.argv[1]
bfbfile = sys.argv[2]
nodetorshim = {}
for arg in sys.argv[3:]:
node, rshim = arg.split(':')
nodetorshim[node] = rshim
installprocs = {}
pipedesc = {}
all = set()
poller = select.epoll()
exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller)
rdy = poller.poll(10)
pendingexecs = []
exitcode = 0
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
@@ -47,38 +47,113 @@ c_crypt.restype = ctypes.c_char_p
def get_my_addresses():
nlhdrsz = struct.calcsize('IHHII')
ifaddrsz = struct.calcsize('BBBBI')
# RTM_GETADDR = 22
# nlmsghdr struct: u32 len, u16 type, u16 flags, u32 seq, u32 pid
nlhdr = struct.pack('IHHII', nlhdrsz + ifaddrsz, 22, 0x301, 0, 0)
# ifaddrmsg struct: u8 family, u8 prefixlen, u8 flags, u8 scope, u32 index
ifaddrmsg = struct.pack('BBBBI', 0, 0, 0, 0, 0)
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((0, 0))
s.sendall(nlhdr + ifaddrmsg)
addrs = []
while True:
pdata = s.recv(65536)
v = memoryview(pdata)
if struct.unpack('H', v[4:6])[0] == 3: # netlink done message
break
while len(v):
length, typ = struct.unpack('IH', v[:6])
if typ == 20:
fam, plen, _, scope, ridx = struct.unpack('BBBBI', v[nlhdrsz:nlhdrsz+ifaddrsz])
if scope in (253, 0):
rta = v[nlhdrsz+ifaddrsz:length]
while len(rta):
rtalen, rtatyp = struct.unpack('HH', rta[:4])
if rtalen < 4:
break
if rtatyp == 1:
addrs.append((fam, rta[4:rtalen], plen, ridx))
rta = rta[msg_align(rtalen):]
v = v[msg_align(length):]
for ifa in get_ifaddrs():
if ifa[0] == 'ip':
addrs.append((ifa[1], ifa[2], ifa[3]))
return addrs
def get_mac_addresses():
macs = []
for ifa in get_ifaddrs():
if ifa[0] == 'ETHER':
macs.append((ifa[1], ifa[2]))
return macs
def get_ifaddrs():
class sockaddr(ctypes.Structure):
_fields_ = [
('sa_family', ctypes.c_uint16),
('sa_data', ctypes.c_ubyte * 14),
]
class sockaddr_in(ctypes.Structure):
_fields_ = [
('sin_family', ctypes.c_uint16),
('sin_port', ctypes.c_uint16),
('sin_addr', ctypes.c_ubyte * 4),
('sin_zero', ctypes.c_ubyte * 8),
]
class sockaddr_in6(ctypes.Structure):
_fields_ = [
('sin6_family', ctypes.c_uint16),
('sin6_port', ctypes.c_uint16),
('sin6_flowinfo', ctypes.c_uint32),
('sin6_addr', ctypes.c_ubyte * 16),
('sin6_scope_id', ctypes.c_uint32),
]
class sockaddr_ll(ctypes.Structure):
_fields_ = [
('sll_family', ctypes.c_uint16),
('sll_protocol', ctypes.c_uint16),
('sll_ifindex', ctypes.c_int32),
('sll_hatype', ctypes.c_uint16),
('sll_pkttype', ctypes.c_uint8),
('sll_halen', ctypes.c_uint8),
('sll_addr', ctypes.c_ubyte * 8),
]
class ifaddrs(ctypes.Structure):
pass
ifaddrs._fields_ = [
('ifa_next', ctypes.POINTER(ifaddrs)),
('ifa_name', ctypes.c_char_p),
('ifa_flags', ctypes.c_uint),
('ifa_addr', ctypes.POINTER(sockaddr)),
('ifa_netmask', ctypes.POINTER(sockaddr)),
('ifa_ifu', ctypes.POINTER(sockaddr)),
('ifa_data', ctypes.c_void_p),
]
libc = ctypes.CDLL(ctypes.util.find_library('c'))
libc.getifaddrs.argtypes = [ctypes.POINTER(ctypes.POINTER(ifaddrs))]
libc.getifaddrs.restype = ctypes.c_int
libc.freeifaddrs.argtypes = [ctypes.POINTER(ifaddrs)]
libc.freeifaddrs.restype = None
ifap = ctypes.POINTER(ifaddrs)()
result = libc.getifaddrs(ctypes.pointer(ifap))
if result != 0:
return []
addresses = []
ifa = ifap
try:
while ifa:
if ifa.contents.ifa_addr:
family = ifa.contents.ifa_addr.contents.sa_family
name = ifa.contents.ifa_name.decode('utf-8') if ifa.contents.ifa_name else None
if family in (socket.AF_INET, socket.AF_INET6):
# skip loopback and non-multicast interfaces
if ifa.contents.ifa_flags & 8 or not ifa.contents.ifa_flags & 0x1000:
ifa = ifa.contents.ifa_next
continue
if family == socket.AF_INET:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in))
addr_bytes = bytes(addr_ptr.contents.sin_addr)
if_index = socket.if_nametoindex(name) if name else 0
addresses.append(('ip', family, addr_bytes, if_index))
elif family == socket.AF_INET6:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in6))
addr_bytes = bytes(addr_ptr.contents.sin6_addr)
scope_id = addr_ptr.contents.sin6_scope_id
addresses.append(('ip', family, addr_bytes, scope_id))
elif family == socket.AF_PACKET:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_ll))
halen = addr_ptr.contents.sll_halen
if addr_ptr.contents.sll_hatype in (1, 32) and halen > 0: # ARPHRD_ETHER or ARPHRD_INFINIBAND
if addr_ptr.contents.sll_hatype == 1 and addr_ptr.contents.sll_addr[0] & 2: # skip locally administered MACs
ifa = ifa.contents.ifa_next
continue
mac_bytes = bytes(addr_ptr.contents.sll_addr[:halen])
macaddr = ':'.join('{:02x}'.format(b) for b in mac_bytes)
addresses.append(('ETHER', name, macaddr))
ifa = ifa.contents.ifa_next
finally:
libc.freeifaddrs(ifap)
return addresses
def scan_confluents(confuuid=None):
srvs = {}
@@ -92,22 +167,24 @@ def scan_confluents(confuuid=None):
s4.bind(('0.0.0.0', 1900))
doneidxs = set([])
msg = 'M-SEARCH * HTTP/1.1\r\nST: urn:xcat.org:service:confluent:'
if not confuuid:
if not confuuid and os.path.exists('/etc/confluent/confluent.deploycfg'):
with open('/etc/confluent/confluent.deploycfg') as dcfg:
for line in dcfg.read().split('\n'):
if line.startswith('confluent_uuid:'):
confluentuuid = line.split(': ')[1]
msg += '/confluentuuid=' + confluentuuid
break
if not confuuid and os.path.exists('/confluent_uuid'):
with open('/confluent_uuid') as cuuidin:
confluentuuid = cuuidin.read().strip()
msg += '/confluentuuid=' + confluentuuid
try:
with open('/sys/devices/virtual/dmi/id/product_uuid') as uuidin:
msg += '/uuid=' + uuidin.read().strip()
except Exception:
pass
for addrf in glob.glob('/sys/class/net/*/address'):
with open(addrf) as addrin:
hwaddr = addrin.read().strip()
msg += '/mac=' + hwaddr
for iface, hwaddr in get_mac_addresses():
msg += '/mac=' + hwaddr
msg = msg.encode('utf8')
for addr in get_my_addresses():
if addr[0] == socket.AF_INET6:
@@ -155,7 +232,8 @@ def scan_confluents(confuuid=None):
if currip.startswith('fe80::') and '%' not in currip:
currip = '{0}%{1}'.format(currip, peer[-1])
srvs[currip] = current
srvlist.append(currip)
if currip not in srvlist:
srvlist.append(currip)
r = select.select((s4, s6), (), (), 2)
if r:
r = r[0]
@@ -625,4 +703,7 @@ if __name__ == '__main__':
elif checkonly:
sys.stdout.write(mclient.check_connections())
else:
sys.stdout.write(mclient.grab_url(sys.argv[1], data).decode())
try:
sys.stdout.buffer.write(mclient.grab_url(sys.argv[1], data))
except AttributeError:
sys.stdout.write(mclient.grab_url(sys.argv[1], data))
@@ -356,7 +356,7 @@ class NetworkManager(object):
currteam = deats.get('connection.master', None)
if currteam == team:
return
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname'):
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname', 'ipv4.dns-search', 'ipv6.dns-search'):
if deats.get(stg, None):
bondcfg[stg] = deats[stg]
if member in self.uuidbyname:
@@ -488,15 +488,16 @@ if __name__ == '__main__':
continue
myname = s.getsockname()
s.close()
curridx = None
if len(myname) == 4:
curridx = myname[-1]
else:
myname = myname[0]
myname = socket.inet_pton(socket.AF_INET, myname)
for addr in myaddrs:
if myname == addr[1].tobytes():
if myname == addr[1]:
curridx = addr[-1]
if curridx in doneidxs:
if curridx is not None and curridx in doneidxs:
continue
for tries in (1, 2, 3):
try:
@@ -55,4 +55,9 @@ confluentpython $confapiclient /confluent-api/self/nodelist | sed -e 's/^- //' >
cat /etc/ssh/shosts.equiv > /root/.shosts
cd -
rm -rf $TMPDIR
systemctl try-restart sshd
# ssh may be sshd or ssh, depending
if systemctl list-unit-files | grep -q '^sshd\.service'; then
systemctl try-restart sshd
elif systemctl list-unit-files | grep -q '^ssh\.service'; then
systemctl try-restart ssh
fi
@@ -26,13 +26,12 @@ mkdir -p opt/confluent/bin
mkdir -p stateless-bin
cp -a el8bin/* .
ln -s el8 el9
ln -s el8 el10
mv el10/initramfs/usr el10/initramfs/var
cp -a el8 el10
cp -a debian debian13
mkdir -p debian13/initramfs/usr
mv debian13/initramfs/lib debian13/initramfs/usr/
mv el10/initramfs/usr el10/initramfs/var
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9 el10; do
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
mkdir ${os}out
cd ${os}out
if [ -d ../${os}bin ]; then
@@ -48,7 +47,7 @@ for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu20.04 ubuntu22.04 u
mv ../addons.cpio .
cd ..
done
for os in el7 el8 suse15 el9 el10 ubuntu20.04; do
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
mkdir ${os}disklessout
cd ${os}disklessout
if [ -d ../${os}bin ]; then
@@ -86,7 +85,10 @@ cp -a esxi7 esxi8
%install
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
#cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 ubuntu20.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 coreos; do
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/common
cp common/initramfs/opt/confluent/bin/apiclient %{buildroot}/opt/confluent/lib/osdeploy/common
for os in rhvh4 el7 el8 el9 el10 debian debian13 genesis suse15 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 coreos; do
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs/aarch64/
if [ -d ${os}disklessout ]; then
@@ -33,7 +33,7 @@ cp -a debian debian13
mkdir -p debian13/initramfs/usr
mv debian13/initramfs/lib debian13/initramfs/usr/
mv el10/initramfs/usr el10/initramfs/var
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 coreos el9 el10; do
for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 coreos el9 el10; do
mkdir ${os}out
cd ${os}out
if [ -d ../${os}bin ]; then
@@ -49,7 +49,7 @@ for os in rhvh4 el7 genesis el8 suse15 debian debian13 ubuntu18.04 ubuntu20.04 u
mv ../addons.cpio .
cd ..
done
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04; do
for os in el7 el8 suse15 el9 el10 ubuntu20.04 ubuntu22.04 ubuntu24.04 ubuntu26.04; do
mkdir ${os}disklessout
cd ${os}disklessout
if [ -d ../${os}bin ]; then
@@ -89,7 +89,7 @@ cp -a esxi7 esxi9
%install
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 esxi6 esxi7 esxi8 esxi9 coreos; do
for os in rhvh4 el7 el8 el9 el10 genesis suse15 ubuntu20.04 debian debian13 ubuntu18.04 ubuntu22.04 ubuntu24.04 ubuntu26.04 esxi6 esxi7 esxi8 esxi9 coreos; do
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
mkdir -p %{buildroot}/opt/confluent/lib/osdeploy/$os/profiles
cp ${os}out/addons.* %{buildroot}/opt/confluent/lib/osdeploy/$os/initramfs
@@ -3,8 +3,8 @@ confluent_whost=$confluent_mgr
if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
confluent_whost="[$confluent_mgr]"
fi
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
mount -t tmpfs untethered /mnt/remoteimg
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
else
@@ -40,20 +40,53 @@ fi
#mount -t tmpfs overlay /mnt/overlay
modprobe zram
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
echo $memtot > /sys/block/zram0/disksize
mkfs.xfs /dev/zram0 > /dev/null
mount -o discard /dev/zram0 /mnt/overlay
if [ ! -f /tmp/mountparts.sh ]; then
mkdir -p /mnt/overlay/upper /mnt/overlay/work
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
if [ ! "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
modprobe zram
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
echo $memtot > /sys/block/zram0/disksize
mkfs.xfs /dev/zram0 > /dev/null
fi
TETHERED=0
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
mount -o discard /dev/zram0 /sysroot
else
mount -t tmpfs disklessroot /sysroot
fi
echo -en "Decrypting and extracting root filesystem: 0%\r"
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
dstsz=$(du -sk /sysroot | awk '{print $1}')
pct=$((dstsz * 100 / srcsz))
if [ $pct -gt 99 ]; then
pct=99
fi
echo -en "Decrypting and extracting root filesystem: $pct%\r"
sleep 0.25
done &
cp -ax /mnt/remote/* /sysroot/
umount /mnt/remote
if [ -e /dev/mapper/cryptimg ]; then
dmsetup remove cryptimg
fi
losetup -d $loopdev
rm /mnt/remoteimg/rootimg.sfs
umount /mnt/remoteimg
wait
echo -e "Decrypting and extracting root filesystem: 100%"
else
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
done
TETHERED=1
mount -o discard /dev/zram0 /mnt/overlay
if [ ! -f /tmp/mountparts.sh ]; then
mkdir -p /mnt/overlay/upper /mnt/overlay/work
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
else
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
done
fi
fi
mkdir -p /sysroot/etc/ssh
mkdir -p /sysroot/etc/confluent
@@ -109,7 +142,7 @@ echo ' EnableSSHKeysign yes' >> $sshconf
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
curl -sf -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_whost/confluent-api/self/nodelist > /sysroot/etc/ssh/shosts.equiv
cp /sysroot/etc/ssh/shosts.equiv /sysroot/root/.shosts
chmod 640 /sysroot/etc/ssh/*_key
chmod 600 /sysroot/etc/ssh/*_key
cp /tls/*.pem /sysroot/etc/pki/ca-trust/source/anchors/
chroot /sysroot/ update-ca-trust
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.service > /sysroot/etc/systemd/system/onboot.service
@@ -129,10 +162,25 @@ ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
mv /lib/firmware /lib/firmware-ramfs
ln -s /sysroot/lib/firmware /lib/firmware
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
if [ $TETHERED -eq 1 ]; then
(
sleep 86400 &
ONBOOTPID=$!
mkdir -p /run/confluent
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
wait $ONBOOTPID
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
) &
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
sleep 0.1
done
else
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
fi
if grep debugssh /proc/cmdline >& /dev/null; then
exec /opt/confluent/bin/start_root
else
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
fi
@@ -21,17 +21,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
fi
if [ -f /tmp/timeservers ]; then
ntpsrvs=$(cat /tmp/timeservers)
sed -i "1,/^pool * /c\\
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
cat /tmp/timeservers >> /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
fi
@@ -4,10 +4,12 @@ if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
confluent_whost="[$confluent_mgr]"
fi
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
TETHERED=0
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
mount -t tmpfs untethered /mnt/remoteimg
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
else
TETHERED=1
confluent_urls="$confluent_urls https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs"
/opt/confluent/bin/urlmount $confluent_urls /mnt/remoteimg
fi
@@ -130,4 +132,17 @@ ln -s /sysroot/lib/modules/$(uname -r) /lib/modules/
mv /lib/firmware /lib/firmware-ramfs
ln -s /sysroot/lib/firmware /lib/firmware
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
if [ $TETHERED -eq 1 ]; then
(
sleep 86400 &
ONBOOTPID=$!
mkdir -p /run/confluent
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
wait $ONBOOTPID
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
) &
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
sleep 0.1
done
fi
exec /opt/confluent/bin/start_root
@@ -27,17 +27,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
fi
if [ -f /tmp/timeservers ]; then
ntpsrvs=$(cat /tmp/timeservers)
sed -i "1,/^pool * /c\\
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
cat /tmp/timeservers >> /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
fi
@@ -68,5 +61,14 @@ run_remote_parts onboot.d
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
run_remote_config onboot.d
if [ -f /run/confluent/onboot_sleep.pid ]; then
loopdev=$(losetup -j /mnt/remoteimg/rootimg.sfs|cut -d: -f 1)
if [ -n "$loopdev" ]; then
losetup "$loopdev" --direct-io=on
fi
sleeppid=$(cat /run/confluent/onboot_sleep.pid)
kill "$sleeppid"
rm -f /run/confluent/onboot_sleep.pid
fi
#curl -X POST -d 'status: booted' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
kill $logshowpid
@@ -277,7 +277,10 @@ def synchronize():
try:
uid = pwd.getpwnam(opts[fname][opt]['name']).pw_uid
except KeyError:
uid = opts[fname][opt]['id']
try:
uid = opts[fname][opt]['id']
except KeyError:
raise Exception(f"Unable to map owner of {fname}")
elif opt == 'group':
try:
gid = grp.getgrnam(opts[fname][opt]['name']).gr_gid
@@ -3,7 +3,7 @@ sed -i 's/centos/CentOS/; s/rhel/Red Hat Enterprise Linux/; s/oraclelinux/Oracle
if grep Fedora $2/profile.yaml > /dev/null; then
sed -i 's/@^minimal-environment/#/' $2/packagelist
fi
if grep ^label: $2/profile.yaml | grep 10 > /dev/null; then
if grep ^label: $2/profile.yaml | grep ' 10' > /dev/null; then
echo 'echo openssh-keysign >> /tmp/addonpackages' > $2/scripts/pre.d/enablekeysign
chmod 644 $2/scripts/pre.d/enablekeysign
fi
@@ -467,7 +467,7 @@ def install_to_disk(imgpath):
subprocess.check_call(['pvcreate', '-ff', '-y', lvmpart])
subprocess.check_call(['vgcreate', vgname, lvmpart])
vgroupmap = {}
if yaml and vgmap:
if yaml and vgmap and os.path.exists('/tmp/volumegroupmap.yml'):
with open('/tmp/volumegroupmap.yml') as mapin:
vgroupmap = yaml.safe_load(mapin)
donedisks = {}
@@ -3,8 +3,8 @@ confluent_whost=$confluent_mgr
if [[ "$confluent_whost" == *:* ]] && [[ "$confluent_whost" != "["* ]]; then
confluent_whost="[$confluent_mgr]"
fi
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay /sysroot
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
mount -t tmpfs untethered /mnt/remoteimg
curl https://$confluent_whost/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
else
@@ -40,20 +40,53 @@ fi
#mount -t tmpfs overlay /mnt/overlay
modprobe zram
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
echo $memtot > /sys/block/zram0/disksize
mkfs.xfs /dev/zram0 > /dev/null
mount -o discard /dev/zram0 /mnt/overlay
if [ ! -f /tmp/mountparts.sh ]; then
mkdir -p /mnt/overlay/upper /mnt/overlay/work
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
if [ ! "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
modprobe zram
memtot=$(grep ^MemTotal: /proc/meminfo|awk '{print $2}')
memtot=$((memtot/2))$(grep ^MemTotal: /proc/meminfo | awk '{print $3'})
echo $memtot > /sys/block/zram0/disksize
mkfs.xfs /dev/zram0 > /dev/null
fi
TETHERED=0
if [ "untethered" = "$(getarg confluent_imagemethod)" -o "uncompressed" = "$(getarg confluent_imagemethod)" ]; then
if [ "untethered" = "$(getarg confluent_imagemethod)" ]; then
mount -o discard /dev/zram0 /sysroot
else
mount -t tmpfs disklessroot /sysroot
fi
echo -en "Decrypting and extracting root filesystem: 0%\r"
srcsz=$(du -sk /mnt/remote | awk '{print $1}')
while [ -f /mnt/remoteimg/rootimg.sfs ]; do
dstsz=$(du -sk /sysroot | awk '{print $1}')
pct=$((dstsz * 100 / srcsz))
if [ $pct -gt 99 ]; then
pct=99
fi
echo -en "Decrypting and extracting root filesystem: $pct%\r"
sleep 0.25
done &
cp -ax /mnt/remote/* /sysroot/
umount /mnt/remote
if [ -e /dev/mapper/cryptimg ]; then
dmsetup remove cryptimg
fi
losetup -d $loopdev
rm /mnt/remoteimg/rootimg.sfs
umount /mnt/remoteimg
wait
echo -e "Decrypting and extracting root filesystem: 100%"
else
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
done
TETHERED=1
mount -o discard /dev/zram0 /mnt/overlay
if [ ! -f /tmp/mountparts.sh ]; then
mkdir -p /mnt/overlay/upper /mnt/overlay/work
mount -t overlay -o upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work,lowerdir=/mnt/remote disklessroot /sysroot
else
for srcmount in $(cat /tmp/mountparts.sh | awk '{print $3}'); do
mkdir -p /mnt/overlay${srcmount}/upper /mnt/overlay${srcmount}/work
mount -t overlay -o upperdir=/mnt/overlay${srcmount}/upper,workdir=/mnt/overlay${srcmount}/work,lowerdir=${srcmount} disklesspart /sysroot${srcmount#/mnt/remote}
done
fi
fi
mkdir -p /sysroot/etc/ssh
mkdir -p /sysroot/etc/confluent
@@ -109,8 +142,10 @@ echo ' EnableSSHKeysign yes' >> $sshconf
echo ' HostbasedKeyTypes *ed25519*' >> $sshconf
curl -sf -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" https://$confluent_whost/confluent-api/self/nodelist > /sysroot/etc/ssh/shosts.equiv
cp /sysroot/etc/ssh/shosts.equiv /sysroot/root/.shosts
chmod 640 /sysroot/etc/ssh/*_key
chroot /sysroot chgrp ssh_keys /etc/ssh/*_key
if grep ^ssh_keys: /etc/group > /dev/null; then
chmod 640 /sysroot/etc/ssh/*_key
chroot /sysroot chgrp ssh_keys /etc/ssh/*_key
fi
cp /tls/*.pem /sysroot/etc/pki/ca-trust/source/anchors/
chroot /sysroot/ update-ca-trust
curl -sf https://$confluent_whost/confluent-public/os/$confluent_profile/scripts/onboot.service > /sysroot/etc/systemd/system/onboot.service
@@ -131,9 +166,35 @@ mv /lib/firmware /lib/firmware-ramfs
ln -s /sysroot/lib/firmware /lib/firmware
rm -f /sysroot/etc/dracut.conf.d/diskless.conf # remove diskless dracut from runtime, to make kdump happier
kill $(grep -l ^/usr/lib/systemd/systemd-udevd /proc/*/cmdline|cut -d/ -f 3)
if grep debugssh /proc/cmdline >& /dev/null; then
debugssh=1
else
debugssh=0
fi
if [ $TETHERED -eq 1 ]; then
# In tethered mode, the double-caching is useful to get through tricky part of
# onboot with confignet. After that, it's excessive cache usage.
# Give the onboot script a hook to have us come in and enable directio to the
# squashfs and drop the cache of the rootimg so far
(
sleep 86400 &
ONBOOTPID=$!
mkdir -p /run/confluent
echo $ONBOOTPID > /run/confluent/onboot_sleep.pid
wait $ONBOOTPID
dd if=/mnt/remoteimg/rootimg.sfs iflag=nocache count=0 >& /dev/null
if [ $debugssh -eq 0 ]; then
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
fi
) &
while [ ! -f /run/confluent/onboot_sleep.pid ]; do
sleep 0.1
done
elif [ $debugssh -eq 0 ]; then
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
fi
if grep debugssh /proc/cmdline >& /dev/null; then
exec /opt/confluent/bin/start_root
else
rm -rf /lib/modules/$(uname -r) /lib/modules/$(uname -r)-ramfs /lib/firmware-ramfs /usr/lib64/libcrypto.so* /usr/lib64/systemd/ /kernel/ /usr/bin/ /usr/sbin/ /usr/libexec/
exec /opt/confluent/bin/start_root -s # share mount namespace, keep kernel callbacks intact
fi
@@ -26,17 +26,10 @@ if grep ^ntpservers: /etc/confluent/confluent.deploycfg > /dev/null; then
fi
if [ -f /tmp/timeservers ]; then
ntpsrvs=$(cat /tmp/timeservers)
sed -i "1,/^pool * /c\\
${ntpsrvs//$'\n'/\\$'\n'}" /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
sed -i '/^[[:space:]]*\(pool\|server\)[[:space:]]/d' /etc/chrony.conf
cat /tmp/timeservers >> /etc/chrony.conf
systemctl restart chronyd
rm -f /tmp/timeservers
fi
export nodename confluent_mgr confluent_profile
@@ -62,5 +55,15 @@ run_remote_parts onboot.d
# Induce execution of remote configuration, e.g. ansible plays in ansible/onboot.d/
run_remote_config onboot.d
if [ -f /run/confluent/onboot_sleep.pid ]; then
loopdev=$(losetup -j /mnt/remoteimg/rootimg.sfs|cut -d: -f 1)
if [ -n "$loopdev" ]; then
losetup "$loopdev" --direct-io=on
fi
sleeppid=$(cat /run/confluent/onboot_sleep.pid)
kill "$sleeppid"
rm -f /run/confluent/onboot_sleep.pid
fi
#curl -X POST -d 'status: booted' -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" https://$confluent_mgr/confluent-api/self/updatestatus
kill $logshowpid
@@ -56,7 +56,7 @@ cat /tls/*.pem > /etc/pki/tls/certs/ca-bundle.crt
TRIES=0
touch /etc/confluent/confluent.info
TRIES=5
echo -n "Waitiing for disks..."
echo -n "Waiting for disks..."
while [ ! -e /dev/disk/by-label ] && [ $TRIES -gt 0 ]; do
sleep 1
TRIES=$((TRIES - 1))
@@ -0,0 +1,29 @@
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
# subdirectory other than public.
#
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
#
# This file lists files to synchronize or merge to the deployed systems from the deployment server
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
# Note particularly the use of '->' to delineate source from target.
# /some/path/hosts -> /etc/hosts
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
# /etc/hosts
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
# owner, group, and permissions in octal notation:
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
# contain the data in the source already in its entirety. This allows append in a fashion that
# is friendly to being run repeatedly
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
MERGE:
# /etc/passwd
# /etc/group
@@ -0,0 +1,29 @@
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
# subdirectory other than public.
#
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
#
# This file lists files to synchronize or merge to the deployed systems from the deployment server
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
# Note particularly the use of '->' to delineate source from target.
# /some/path/hosts -> /etc/hosts
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
# /etc/hosts
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
# owner, group, and permissions in octal notation:
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
# contain the data in the source already in its entirety. This allows append in a fashion that
# is friendly to being run repeatedly
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
MERGE:
# /etc/passwd
# /etc/group
@@ -7,7 +7,7 @@ exec >> /target/var/log/confluent/confluent-firstboot.log
exec 2>> /target/var/log/confluent/confluent-firstboot.log
chmod 600 /target/var/log/confluent/confluent-firstboot.log
cp -a /etc/confluent/ssh/* /etc/ssh/
systemctl restart sshd
systemctl restart ssh
rootpw=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg |awk '{print $2}')
if [ ! -z "$rootpw" -a "$rootpw" != "null" ]; then
echo root:$rootpw | chpasswd -e
@@ -33,7 +33,7 @@ done
echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/confluent.conf
echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/confluent.conf
echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/confluent.conf
systemctl restart sshd
systemctl restart ssh
mkdir -p /etc/confluent
export nodename confluent_profile confluent_mgr
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /etc/confluent/functions
@@ -0,0 +1,29 @@
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
# subdirectory other than public.
#
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
#
# This file lists files to synchronize or merge to the deployed systems from the deployment server
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
# Note particularly the use of '->' to delineate source from target.
# /some/path/hosts -> /etc/hosts
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
# /etc/hosts
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
# owner, group, and permissions in octal notation:
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
# contain the data in the source already in its entirety. This allows append in a fashion that
# is friendly to being run repeatedly
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
MERGE:
# /etc/passwd
# /etc/group
@@ -37,10 +37,20 @@ else
chroot . custom-installation/confluent/bin/clortho $NODENAME $MGR > /root/custom-installation/confluent/confluent.apikey
APIKEY=$(cat /root/custom-installation/confluent/confluent.apikey)
done
MGR=[$MGR]
nic=$(grep ^MANAGER /custom-installation/confluent/confluent.info|grep fe80::|sed -e s/.*%//|head -n 1)
nic=$(ip link |grep ^$nic:|awk '{print $2}')
DEVICE=${nic%:}
if echo "$MGR" | grep -q ':'; then
# IPv6 manager: wrap address in brackets and resolve interface from scoped manager entry.
MGR=[$MGR]
nic=$(grep ^MANAGER /custom-installation/confluent/confluent.info|grep fe80::|sed -e s/.*%//|head -n 1)
nic=$(ip link |grep ^$nic:|awk '{print $2}')
DEVICE=${nic%:}
else
# IPv4 routed deployment: use previously detected NIC, fallback to route lookup.
if [ -f /tmp/autodetectnic ]; then
DEVICE=$(cat /tmp/autodetectnic)
else
DEVICE=$(ip route get ${MGR} 2>/dev/null | head -1 | sed -n 's/.*dev \([^ ]*\).*/\1/p')
fi
fi
IP=done
fi
if [ -z "$MGTIFACE" ]; then
@@ -97,6 +97,62 @@ while ! grep NODENAME /custom-installation/confluent/confluent.info; do
echo -n $(grep ^apitoken: cnflnt.yml|awk '{print $2}') > $hmackeyfile
cd -
umount $tmnt
elif confluentsrv=$(sed -n 's/.*confluent=\([^ ]*\).*/\1/p' /proc/cmdline); [ ! -z "$confluentsrv" ]; then
echo "confluent= kernel arg found: $confluentsrv" > /dev/console 2>&1
. /scripts/functions
rmmod cdc_ether 2> /dev/null
rm -rf /run/net* /run/dhcpcd /var/lib/dhcpcd
for dev in $(ip a|grep MULTICAST|awk '{print $2}'|sed -e s/://); do
ip a flush $dev
echo 1 > /proc/sys/net/ipv6/conf/$dev/addr_gen_mode 2>/dev/null
echo 0 > /proc/sys/net/ipv6/conf/$dev/addr_gen_mode 2>/dev/null
done
unset DEVICE DEVICE6 IP IP6 dev
echo "Starting DHCP configure_networking..." > /dev/console 2>&1
configure_networking
echo "DHCP done, DEVICE=$DEVICE" > /dev/console 2>&1
echo $DEVICE > /tmp/autodetectnic
RETRIES=0
while [ $RETRIES -lt 5 ]; do
if openssl s_client -connect $confluentsrv:443 </dev/null > /dev/null 2>&1; then
echo "TLS connectivity to $confluentsrv OK" > /dev/console 2>&1
break
fi
RETRIES=$((RETRIES + 1))
echo "Cannot reach $confluentsrv:443, retry $RETRIES/5..." > /dev/console 2>&1
sleep 3
done
if [ $RETRIES -ge 5 ]; then
echo "Failed to reach $confluentsrv after 5 retries, falling back to copernicus" > /dev/console 2>&1
/opt/confluent/bin/copernicus -t > /custom-installation/confluent/confluent.info
continue
fi
myids="uuid=$(cat /sys/devices/virtual/dmi/id/product_uuid)"
for mac in $(ip link | grep 'link/ether' | awk '{print $2}'); do
myids="$myids/mac=$mac"
done
echo "Calling whoami with IDs: $myids" > /dev/console 2>&1
myname=$( (printf "GET /confluent-api/self/whoami HTTP/1.0\r\nHost: $confluentsrv\r\nCONFLUENT_IDS: $myids\r\n\r\n"; sleep 3) \
| openssl s_client -connect $confluentsrv:443 -quiet 2>/dev/null \
| tail -1 | tr -d '\r\n')
echo "whoami returned: '$myname'" > /dev/console 2>&1
if [ ! -z "$myname" ]; then
MGR=$confluentsrv
echo "NODENAME: $myname" > /custom-installation/confluent/confluent.info
echo "MANAGER: $confluentsrv" >> /custom-installation/confluent/confluent.info
echo "EXTMGRINFO: $confluentsrv||1" >> /custom-installation/confluent/confluent.info
else
echo "whoami returned empty, retrying in 10s..." > /dev/console 2>&1
sleep 10
fi
else
/opt/confluent/bin/copernicus -t > /custom-installation/confluent/confluent.info
fi
@@ -28,7 +28,7 @@ done
echo HostbasedAuthentication yes >> /etc/ssh/sshd_config.d/confluent.conf
echo HostbasedUsesNameFromPacketOnly yes >> /etc/ssh/sshd_config.d/confluent.conf
echo IgnoreRhosts no >> /etc/ssh/sshd_config.d/confluent.conf
systemctl restart sshd
systemctl restart ssh
mkdir -p /etc/confluent
export nodename confluent_profile confluent_mgr
curl -f https://$confluent_mgr/confluent-public/os/$confluent_profile/scripts/functions > /etc/confluent/functions
@@ -0,0 +1,29 @@
# It is advised to avoid /var/lib/confluent/public as a source for syncing. /var/lib/confluent/public
# is served without authentication and thus any sensitive content would be a risk. If wanting to host
# syncfiles on a common share, it is suggested to have /var/lib/confluent be the share and use some other
# subdirectory other than public.
#
# Syncing is performed as the 'confluent' user, so all source files must be accessible by the confluent user.
#
# This file lists files to synchronize or merge to the deployed systems from the deployment server
# To specify taking /some/path/hosts on the deployment server and duplicating it to /etc/hosts:
# Note particularly the use of '->' to delineate source from target.
# /some/path/hosts -> /etc/hosts
# If wanting to simply use the same path for source and destinaiton, the -> may be skipped:
# /etc/hosts
# More function is available, for example to limit the entry to run only on n1 through n8, and to set
# owner, group, and permissions in octal notation:
# /example/source -> n1-n8:/etc/target (owner=root,group=root,permissions=600)
# Entries under APPENDONCE: will be added to specified target, only if the target does not already
# contain the data in the source already in its entirety. This allows append in a fashion that
# is friendly to being run repeatedly
# Entries under MERGE: will attempt to be intelligently merged. This supports /etc/group and /etc/passwd
# Any supporting entries in /etc/shadow or /etc/gshadow are added automatically, with password disabled
# It also will not inject 'system' ids (under 1,000 usually) as those tend to be local and rpm managed.
MERGE:
# /etc/passwd
# /etc/group
+1
View File
@@ -0,0 +1 @@
ubuntu22.04/
+1
View File
@@ -0,0 +1 @@
ubuntu20.04-diskless/
+151 -27
View File
@@ -7,6 +7,7 @@
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#define COM1 0x3f8
#define COM2 0x2f8
@@ -19,6 +20,137 @@
#define SPEED57600 6
#define SPEED115200 7
typedef struct {
char devnode[32];
speed_t speed;
int valid;
} serial_port_t;
serial_port_t process_spcr() {
serial_port_t result = {0};
char buff[128];
int fd;
uint64_t address;
int currspeed;
result.valid = 0;
fd = open("/sys/firmware/acpi/tables/SPCR", O_RDONLY);
if (fd < 0) {
return result;
}
if (read(fd, buff, 80) < 80) {
close(fd);
return result;
}
close(fd);
if (buff[8] != 2) return result; // revision 2
if (buff[36] != 0) return result; // 16550 only
if (buff[40] != 1) return result; // IO only
address = *(uint64_t *)(buff + 44);
currspeed = buff[58];
if (address == COM1) {
strncpy(result.devnode, "/dev/ttyS0", sizeof(result.devnode));
} else if (address == COM2) {
strncpy(result.devnode, "/dev/ttyS1", sizeof(result.devnode));
} else if (address == COM3) {
strncpy(result.devnode, "/dev/ttyS2", sizeof(result.devnode));
} else if (address == COM4) {
strncpy(result.devnode, "/dev/ttyS3", sizeof(result.devnode));
} else {
return result;
}
if (currspeed == SPEED9600) {
result.speed = B9600;
} else if (currspeed == SPEED19200) {
result.speed = B19200;
} else if (currspeed == SPEED57600) {
result.speed = B57600;
} else if (currspeed == SPEED115200) {
result.speed = B115200;
} else {
return result;
}
result.valid = 1;
return result;
}
serial_port_t identify_by_sys_vendor() {
serial_port_t result = {0};
char buff[128];
FILE *f;
f = fopen("/sys/devices/virtual/dmi/id/sys_vendor", "r");
if (f) {
if (fgets(buff, sizeof(buff), f)) {
if (strstr(buff, "Supermicro")) {
strncpy(result.devnode, "/dev/ttyS1", sizeof(result.devnode));
result.speed = B115200;
result.valid = 1;
}
}
fclose(f);
}
return result;
}
serial_port_t search_serial_ports() {
serial_port_t result = {0};
DIR *dir;
struct dirent *entry;
int fd;
int status;
int numfound= 0;
int numpossible = 0;
dir = opendir("/dev");
if (!dir) {
return result;
}
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, "ttyS", 4) != 0) {
continue;
}
char devpath[64];
snprintf(devpath, sizeof(devpath), "/dev/%s", entry->d_name);
fd = open(devpath, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
continue;
}
if (ioctl(fd, TIOCMGET, &status) == 0) {
numpossible++;
if (numfound < 1) {
strncpy(result.devnode, devpath, sizeof(result.devnode));
result.speed = B115200;
}
if (status & TIOCM_CAR) {
strncpy(result.devnode, devpath, sizeof(result.devnode));
numfound++;
result.speed = B115200;
}
}
close(fd);
}
closedir(dir);
if (numfound == 1 || numpossible == 1) {
result.valid = 1;
}
return result;
}
int main(int argc, char* argv[]) {
struct termios tty;
struct termios tty2;
@@ -36,46 +168,38 @@ int main(int argc, char* argv[]) {
char* offset;
uint64_t address;
bufflen = 0;
tmpi = open("/sys/firmware/acpi/tables/SPCR", O_RDONLY);
if (tmpi < 0) {
exit(0);
}
if (read(tmpi, buff, 80) < 80) {
exit(0);
}
close(tmpi);
if (buff[8] != 2) exit(0); //revision 2
if (buff[36] != 0) exit(0); //16550 only
if (buff[40] != 1) exit(0); //IO only
address = *(uint64_t *)(buff + 44);
currspeed = buff[58];
offset = buff + 10;
if (address == COM1) {
strncpy(buff, "/dev/ttyS0", 128);
} else if (address == COM2) {
strncpy(buff, "/dev/ttyS1", 128);
} else if (address == COM3) {
strncpy(buff, "/dev/ttyS2", 128);
} else if (address == COM4) {
strncpy(buff, "/dev/ttyS3", 128);
} else {
#ifndef __x86_64__
// Only x86 needs autoconsole, other platforms have reasonable default serial console
exit(0);
#endif
serial_port_t spcr = process_spcr();
if (!spcr.valid) {
spcr = search_serial_ports();
}
if (!spcr.valid) {
spcr = identify_by_sys_vendor();
}
if (!spcr.valid) {
exit(0);
}
strncpy(buff, spcr.devnode, sizeof(buff));
offset = strchr(buff, 0);
currspeed = spcr.speed;
ttyf = open(buff, O_RDWR | O_NOCTTY);
if (ttyf < 0) {
fprintf(stderr, "Unable to open tty\n");
exit(1);
}
if (currspeed == SPEED9600) {
if (currspeed == B9600) {
cspeed = B9600;
strncpy(offset, ",9600", 6);
} else if (currspeed == SPEED19200) {
} else if (currspeed == B19200) {
cspeed = B19200;
strncpy(offset, ",19200", 7);
} else if (currspeed == SPEED57600) {
} else if (currspeed == B57600) {
cspeed = B57600;
strncpy(offset, ",57600", 7);
} else if (currspeed == SPEED115200) {
} else if (currspeed == B115200) {
cspeed = B115200;
strncpy(offset, ",115200", 8);
} else {
+3
View File
@@ -49,6 +49,7 @@ def main(args):
wiz.add_argument('-p', help='Copy in TFTP contents required for PXE support', action='store_true')
wiz.add_argument('-i', help='Interactively prompt for behaviors', action='store_true')
wiz.add_argument('-l', help='Set up local management node to allow login from managed nodes', action='store_true')
wiz.add_argument('-r', help='Repack site contents if present', action='store_true')
osip = sp.add_parser('importcheck', help='Check import of an OS image from an ISO image')
osip.add_argument('imagefile', help='File to use for source of importing')
osip = sp.add_parser('import', help='Import an OS image from an ISO image')
@@ -367,6 +368,8 @@ def initialize(cmdset):
rc = initialize_genesis()
if rc != 0:
sys.exit(rc)
if cmdset.r:
didsomething = True
if not didsomething and (cmdset.k or cmdset.l or cmdset.g or cmdset.p):
if cmdset.g:
updateboot('genesis-x86_64')
+1
View File
@@ -94,6 +94,7 @@ _allowedbyrole = {
'/node*/power/state',
'/node*/sensors/*',
'/node*/attributes/current',
'/node*/attributes/all',
'/node*/description',
'/noderange/*/nodes/',
'/nodes/',
+135 -47
View File
@@ -1,11 +1,22 @@
import os
if __name__ == '__main__':
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.collective.manager as collective
import confluent.util as util
from os.path import exists
import datetime
import shutil
import socket
import eventlet.green.subprocess as subprocess
import tempfile
try:
import cryptography.x509 as x509
except ImportError:
x509 = None
def mkdirp(targ):
try:
@@ -178,6 +189,17 @@ def assure_tls_ca():
os.symlink(certname, hashname)
finally:
os.seteuid(ouid)
return certout
#def is_self_signed(pem):
# cert = ssl.PEM_cert_to_DER_cert(pem)
# return cert.get('subjectAltName', []) == cert.get('issuer', [])
# x509 certificate issuer subject comparison..
#>>> b.issuer
#<Name(C=US,ST=NC,L=RTP,O=Lenovo,CN=XCC-7D9D-J102MM2T)>
#>>> b.subject
#<Name(C=US,ST=NC,L=RTP,O=Lenovo,CN=XCC-7D9D-J102MM2T)>
def substitute_cfg(setting, key, val, newval, cfgfile, line):
if key.strip() == setting:
@@ -222,7 +244,7 @@ def create_full_ca(certout):
cfgfile.write(line.strip() + '\n')
continue
cfgfile.write(line.strip() + '\n')
cfgfile.write('\n[CACert]\nbasicConstraints = CA:true\n\n[ca_confluent]\n')
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\nkeyUsage = critical,keyCertSign,cRLSign\n[ca_confluent]\n')
subprocess.check_call(
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout])
@@ -231,7 +253,7 @@ def create_full_ca(certout):
subprocess.check_call(
['openssl', 'ca', '-config', newcfg, '-batch', '-selfsign',
'-extensions', 'CACert', '-extfile', newcfg,
'-notext', '-startdate',
'-notext', '-md', 'sha384', '-startdate',
'19700101010101Z', '-enddate', '21000101010101Z', '-keyfile',
keyout, '-out', '/etc/confluent/tls/ca/cacert.pem', '-in', csrout]
)
@@ -257,7 +279,7 @@ def create_simple_ca(keyout, certout):
if len(subj) > 68:
subj = subj[:68]
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\n[CACert]\nbasicConstraints = CA:true\n')
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\n')
subprocess.check_call([
'openssl', 'req', '-new', '-x509', '-key', keyout, '-days',
'27300', '-out', certout, '-subj', subj,
@@ -266,70 +288,112 @@ def create_simple_ca(keyout, certout):
finally:
os.remove(tmpconfig)
def create_certificate(keyout=None, certout=None, csrout=None):
if not keyout:
def create_certificate(keyout=None, certout=None, csrfile=None, subj=None, san=None, backdate=True, days=None):
now_utc = datetime.datetime.now(datetime.timezone.utc)
if backdate:
# To deal with wildly off clocks, we backdate certificates.
startdate = '20000101010101Z'
else:
# apply a mild backdate anyway, even if these are supposed to be for more accurate clocks
startdate = (now_utc - datetime.timedelta(hours=24)).strftime('%Y%m%d%H%M%SZ')
if days is None:
enddate = '21000101010101Z'
else:
enddate = (now_utc + datetime.timedelta(days=days)).strftime('%Y%m%d%H%M%SZ')
tlsmateriallocation = {}
if not certout:
tlsmateriallocation = get_certificate_paths()
keyout = tlsmateriallocation.get('keys', [None])[0]
certout = tlsmateriallocation.get('certs', [None])[0]
if not certout:
certout = tlsmateriallocation.get('bundles', [None])[0]
if not keyout or not certout:
if (not keyout and not csrfile) or not certout:
raise Exception('Unable to locate TLS certificate path automatically')
assure_tls_ca()
shortname = socket.gethostname().split('.')[0]
longname = shortname # socket.getfqdn()
if not csrout:
cacertname = assure_tls_ca()
if not subj:
shortname = socket.gethostname().split('.')[0]
longname = shortname # socket.getfqdn()
subj = '/CN={0}'.format(longname)
elif '/CN=' not in subj:
subj = '/CN={0}'.format(subj)
if not csrfile:
subprocess.check_call(
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout])
ipaddrs = list(get_ip_addresses())
san = ['IP:{0}'.format(x) for x in ipaddrs]
# It is incorrect to put IP addresses as DNS type. However
# there exists non-compliant clients that fail with them as IP
# san.extend(['DNS:{0}'.format(x) for x in ipaddrs])
dnsnames = set(ipaddrs)
dnsnames.add(shortname)
for currip in ipaddrs:
dnsnames.add(socket.getnameinfo((currip, 0), 0)[0])
for currname in dnsnames:
san.append('DNS:{0}'.format(currname))
#san.append('DNS:{0}'.format(longname))
san = ','.join(san)
permitdomains = []
if x509:
# check if this CA has name constraints, and avoid violating them
with open(cacertname, 'rb') as f:
cer = x509.load_pem_x509_certificate(f.read())
for extension in cer.extensions:
if extension.oid == x509.ExtensionOID.NAME_CONSTRAINTS:
nc = extension.value
for pname in nc.permitted_subtrees:
permitdomains.append(pname.value)
if not san:
ipaddrs = list(get_ip_addresses())
if not permitdomains:
san = ['IP:{0}'.format(x) for x in ipaddrs]
# It is incorrect to put IP addresses as DNS type. However
# there exists non-compliant clients that fail with them as IP
# san.extend(['DNS:{0}'.format(x) for x in ipaddrs])
dnsnames = set(ipaddrs)
dnsnames.add(shortname)
dnsnames.add(longname)
else:
# nameconstraints preclude IP and shortname
san = []
dnsnames = set()
for suffix in permitdomains:
if longname.endswith(suffix):
dnsnames.add(longname)
break
for currip in ipaddrs:
currname = socket.getnameinfo((currip, 0), 0)[0]
for suffix in permitdomains:
if currname.endswith(suffix):
dnsnames.add(currname)
break
if not permitdomains:
dnsnames.add(currname)
for currname in dnsnames:
san.append('DNS:{0}'.format(currname))
#san.append('DNS:{0}'.format(longname))
san = ','.join(san)
sslcfg = get_openssl_conf_location()
tmphdl, tmpconfig = tempfile.mkstemp()
os.close(tmphdl)
tmphdl, extconfig = tempfile.mkstemp()
os.close(tmphdl)
needcsr = False
if csrout is None:
if csrfile is None:
needcsr = True
tmphdl, csrout = tempfile.mkstemp()
tmphdl, csrfile = tempfile.mkstemp()
os.close(tmphdl)
shutil.copy2(sslcfg, tmpconfig)
try:
with open(extconfig, 'a') as cfgfile:
cfgfile.write('\nbasicConstraints=critical,CA:false\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=serverAuth,clientAuth\nsubjectAltName={0}'.format(san))
if needcsr:
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
with open(extconfig, 'a') as cfgfile:
cfgfile.write('\nbasicConstraints=CA:false\nsubjectAltName={0}'.format(san))
subprocess.check_call([
'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj',
'/CN={0}'.format(longname),
'-extensions', 'SAN', '-config', tmpconfig
'openssl', 'req', '-new', '-key', keyout, '-out', csrfile, '-subj',
subj, '-extensions', 'SAN', '-config', tmpconfig
])
else:
# when used manually, allow the csr SAN to stand
# may add explicit subj/SAN argument, in which case we would skip copy
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\ncopy_extensions=copy\n')
with open(extconfig, 'a') as cfgfile:
cfgfile.write('\nbasicConstraints=CA:false\n')
#else:
# # when used manually, allow the csr SAN to stand
# # may add explicit subj/SAN argument, in which case we would skip copy
# #with open(tmpconfig, 'a') as cfgfile:
# # cfgfile.write('\ncopy_extensions=copy\n')
# with open(extconfig, 'a') as cfgfile:
# cfgfile.write('\nbasicConstraints=CA:false\n')
if os.path.exists('/etc/confluent/tls/cakey.pem'):
# simple style CA in effect, make a random serial number and
# hope for the best, and accept inability to backdate the cert
serialnum = '0x' + ''.join(['{:02x}'.format(x) for x in bytearray(os.urandom(20))])
subprocess.check_call([
'openssl', 'x509', '-req', '-in', csrout,
'openssl', 'x509', '-req', '-in', csrfile,
'-CA', '/etc/confluent/tls/cacert.pem',
'-CAkey', '/etc/confluent/tls/cakey.pem',
'-set_serial', serialnum, '-out', certout, '-days', '27300',
@@ -348,12 +412,11 @@ def create_certificate(keyout=None, certout=None, csrout=None):
shutil.copy2(cacfgfile, tmpcafile)
os.close(tmphdl)
cacfgfile = tmpcafile
# with realcalock: # if we put it in server, we must lock it
subprocess.check_call([
'openssl', 'ca', '-config', cacfgfile,
'-in', csrout, '-out', certout, '-batch', '-notext',
'-startdate', '19700101010101Z', '-enddate', '21000101010101Z',
'-extfile', extconfig
'openssl', 'ca', '-config', cacfgfile, '-rand_serial',
'-in', csrfile, '-out', certout, '-batch', '-notext',
'-startdate', startdate, '-enddate', enddate, '-md', 'sha384',
'-extfile', extconfig, '-subj', subj
])
for keycopy in tlsmateriallocation.get('keys', []):
if keycopy != keyout:
@@ -381,18 +444,43 @@ def create_certificate(keyout=None, certout=None, csrout=None):
finally:
os.remove(tmpconfig)
if needcsr:
os.remove(csrout)
print(extconfig) # os.remove(extconfig)
os.remove(csrfile)
os.remove(extconfig)
if __name__ == '__main__':
import sys
import ipaddress
outdir = os.getcwd()
keyout = os.path.join(outdir, 'key.pem')
certout = os.path.join(outdir, sys.argv[2] + 'cert.pem')
certout = os.path.join(outdir, 'cert.pem')
csrout = None
subj, san = (None, None)
try:
bindex = sys.argv.index('-b')
bmcnode = sys.argv.pop(bindex + 1) # Remove bmcnode argument
sys.argv.pop(bindex) # Remove -b flag
import confluent.config.configmanager as cfm
c = cfm.ConfigManager(None)
subj, san = util.get_bmc_subject_san(c, bmcnode)
except ValueError:
bindex = None
if subj is None:
try:
sans = set()
sindex = sys.argv.index('-s')
subj = sys.argv.pop(sindex + 1) # Remove subject argument
sys.argv.pop(sindex) # Remove -s flag
try:
ipaddress.ip_address(subj)
sans.add('IP:{0}'.format(subj))
except ValueError:
sans.add('DNS:{0}'.format(subj))
san = ','.join(sans) if sans else None
except ValueError:
pass
try:
csrout = sys.argv[1]
except IndexError:
csrout = None
create_certificate(keyout, certout, csrout)
create_certificate(keyout, certout, csrout, subj, san, backdate=False, days=3650)
@@ -716,6 +716,7 @@ def become_leader(connection):
if reassimilate is not None:
reassimilate.kill()
reassimilate = eventlet.spawn(reassimilate_missing)
cfm._init_indexes()
cfm._ready = True
if _assimilate_missing(skipaddr):
schedule_rebalance()
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2019 Lenovo
# Copyright 2015-2025 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -408,6 +408,12 @@ node = {
'include /<prefixlen> CIDR suffix to indicate subnet length, which is '
'autodetected by default where possible.',
},
'hardwaremanagement.manager_tls_name': {
'description': 'A name to use in lieu of the value in hardwaremanagement.manager for '
'TLS certificate verification purposes. Some strategies involve a non-IP, '
'non-resolvable name, or this can be used to access by IP while using name-based '
'validation',
},
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
@@ -444,6 +450,9 @@ node = {
#IBM Flex)''',
# 'appliesto': ['system'],
# },
'id.index': {
'description': 'Confluent generated numeric index for the node.',
},
'id.model': {
'description': 'The model number of a node. In scenarios where there '
'is both a name and a model number, it is generally '
@@ -469,17 +478,17 @@ node = {
'the discovery process to decide where to place the mac address of a detected PXE nic.',
},
'net.connection_name': {
'description': 'Name to use when specifiying a name for connection and/or interface name for a team. This may be the name of a team interface, '
'description': 'Name to use when specifiying a name for connection and/or interface name for a team/bond. This may be the name of a team/bond interface, '
'the connection name in network manager for the interface, or may be installed as an altname '
'as supported by the respective OS deployment profiles. Default is to accept default name for '
'a team consistent with the respective OS, or to use the matching original port name as connection name.'
'a team/bond consistent with the respective OS, or to use the matching original port name as connection name.'
},
'net.interface_names': {
'description': 'Interface name or comma delimited list of names to match for this interface. It is generally recommended '
'to leave this blank unless needing to set up interfaces that are not on a common subnet with a confluent server, '
'as confluent servers provide autodetection for matching the correct network definition to an interface. '
'This would be the default name per the deployed OS and can be a comma delimited list to denote members of '
'a team or a single interface for VLAN/PKEY connections.'
'a team/bond or a single interface for VLAN/PKEY connections.'
},
'net.mtu': {
'description': 'MTU to apply to this connection',
@@ -565,7 +574,7 @@ node = {
'operating system',
},
'net.team_mode': {
'description': 'Indicates that this interface should be a team and what mode or runner to use when teamed. '
'description': 'Indicates that this interface should be a team/bond and what mode or runner to use when teamed or bonded. '
'If this covers a deployment interface, one of the member interfaces may be brought up as '
'a standalone interface until deployment is complete, as supported by the OS deployment profile. '
'To support this scenario, the switch should be set up to allow independent operation of member ports (e.g. lacp bypass mode or fallback mode).',
@@ -599,6 +608,10 @@ node = {
'description': ('SNMPv1 community string, it is highly recommended to'
'step up to SNMPv3'),
},
'snmp.privacyprotocol': {
'description': 'The privacy protocol to use for SNMPv3',
'valid_values': ('aes', 'des'),
},
# 'secret.snmplocalizedkey': {
# 'description': ("SNMPv3 key localized to this node's SNMP Engine id"
# 'This can be used in lieu of snmppassphrase to avoid'
@@ -164,6 +164,45 @@ def _mkpath(pathname):
raise
def _count_freeindexes(freeindexes):
count = 0
for idx in freeindexes:
if isinstance(idx, list):
for subidx in range(idx[0], idx[1] + 1):
count += 1
else:
count += 1
return count
def _is_free_index(freeindexes, idx):
for freeidx in freeindexes:
if isinstance(freeidx, list):
if freeidx[0] <= idx <= freeidx[1]:
return True
else:
if freeidx == idx:
return True
return False
def _remove_free_index(freeindexes, idx):
for i, freeidx in enumerate(freeindexes):
if isinstance(freeidx, list):
if freeidx[0] <= idx <= freeidx[1]:
if freeidx[0] == freeidx[1]:
del freeindexes[i]
elif freeidx[0] == idx:
freeindexes[i][0] += 1
elif freeidx[1] == idx:
freeindexes[i][1] -= 1
else:
freeindexes.insert(i + 1, [idx + 1, freeidx[1]])
freeindexes[i][1] = idx - 1
return
else:
if freeidx == idx:
del freeindexes[i]
return
def _derive_keys(password, salt):
#implement our specific combination of pbkdf2 transforms to get at
#key. We bump the iterations up because we can afford to
@@ -334,7 +373,7 @@ def _rpc_rename_nodes(tenant, renamemap):
def _rpc_rename_nodegroups(tenant, renamemap):
ConfigManager(tenant)._true_rename_nodegroups(renamemap)
ConfigManager(tenant)._true_rename_groups(renamemap)
def _rpc_set_group_attributes(tenant, attribmap, autocreate):
@@ -974,6 +1013,14 @@ def del_collective_member(name):
if cfgstreams:
exec_on_followers_unconditional('_true_del_collective_member', name)
_true_del_collective_member(name)
if cfgstreams:
_hasquorum = has_quorum()
pushes = eventlet.GreenPool()
payload = msgpack.packb({'quorum': _hasquorum}, use_bin_type=False)
for _ in pushes.starmap(
_push_rpc,
[(cfgstreams[s]['stream'], payload) for s in cfgstreams]):
pass
def _true_del_collective_member(name, sync=True):
global cfgleader
@@ -1142,7 +1189,10 @@ class _ExpressionFormat(string.Formatter):
# such as 'net.pxe.hwaddr'
key = '.' + left.attr + key
left = left.value
key = left.id + key
if isinstance(left, ast.Name):
key = left.id + key
else:
raise ValueError("Invalid AST structure: expected ast.Name at end of attribute chain")
if (not key.startswith('custom.') and
_get_valid_attrname(key) not in allattributes.node):
raise ValueError(
@@ -1203,6 +1253,32 @@ class _ExpressionFormat(string.Formatter):
return strval[index]
elif isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.Call):
key = ''
baseval = ''
if isinstance(node.func, ast.Attribute):
fun_name = node.func.attr
baseval = self._handle_ast_node(node.func.value)
else:
raise ValueError("Invalid function call syntax in expression")
if fun_name == 'replace':
if len(node.args) != 2:
raise ValueError("Invalid number of arguments to replace")
arg1 = self._handle_ast_node(node.args[0])
arg2 = self._handle_ast_node(node.args[1])
return baseval.replace(arg1, arg2)
elif fun_name == 'upper':
return baseval.upper()
elif fun_name == 'lower':
return baseval.lower()
elif fun_name == 'block_number':
chunk_size = self._handle_ast_node(node.args[0])
return (int(baseval) - 1) // chunk_size + 1
elif fun_name == 'block_offset':
chunk_size = self._handle_ast_node(node.args[0])
return (int(baseval) - 1) % chunk_size + 1
else:
raise ValueError("Unsupported function in expression")
else:
raise ValueError("Unrecognized expression syntax")
@@ -2206,7 +2282,7 @@ class ConfigManager(object):
watcher = self._nodecollwatchers[self.tenant][watcher]
watcher(added=(), deleting=nodes, renamed=(), configmanager=self)
changeset = {}
for node in nodes:
for node in confluent.util.natural_sort(nodes):
# set a reserved attribute for the sake of the change notification
# framework to trigger on
changeset[node] = {'_nodedeleted': 1}
@@ -2214,6 +2290,29 @@ class ConfigManager(object):
if node in self._cfgstore['nodes']:
self._sync_groups_to_node(node=node, groups=[],
changeset=changeset)
nidx = self._cfgstore['nodes'][node].get('id.index', {}).get('value', None)
if nidx is not None:
currmaxidx = get_global('max_node_index')
freeindexes = get_global('free_node_indexes')
if not freeindexes:
freeindexes = []
if nidx == currmaxidx - 1:
currmaxidx = currmaxidx - 1
while _is_free_index(freeindexes, currmaxidx - 1):
_remove_free_index(freeindexes, currmaxidx - 1)
currmaxidx = currmaxidx - 1
set_global('max_node_index', currmaxidx)
else:
lastindex = freeindexes[-1] if freeindexes else [-2, -2]
if not isinstance(lastindex, list):
lastindex = [lastindex, lastindex]
if nidx == lastindex[1] + 1:
lastindex[1] = nidx
if freeindexes:
freeindexes[-1] = lastindex
else:
freeindexes.append(nidx)
set_global('free_node_indexes', freeindexes)
del self._cfgstore['nodes'][node]
_mark_dirtykey('nodes', node, self.tenant)
self._notif_attribwatchers(changeset)
@@ -2491,12 +2590,29 @@ class ConfigManager(object):
attrname, node)
raise ValueError(errstr)
attribmap[node][attrname] = attrval
for node in attribmap:
for node in confluent.util.natural_sort(attribmap):
node = confluent.util.stringify(node)
exprmgr = None
if node not in self._cfgstore['nodes']:
newnodes.append(node)
self._cfgstore['nodes'][node] = {}
freeindexes = get_global('free_node_indexes')
if not freeindexes:
freeindexes = []
if _count_freeindexes(freeindexes) > 128: # tend to leave freed indexes disused until a lot have accumulated
if isinstance(freeindexes[0], list):
nidx = freeindexes[0][0]
freeindexes[0][0] = nidx + 1
if freeindexes[0][0] == freeindexes[0][1]:
freeindexes[0] = freeindexes[0][0]
else:
nidx = freeindexes.pop(0)
set_global('free_node_indexes', freeindexes)
else:
nidx = get_global('max_node_index')
if nidx is None:
nidx = 1
set_global('max_node_index', nidx + 1)
self._cfgstore['nodes'][node] = {'id.index': {'value': nidx}}
cfgobj = self._cfgstore['nodes'][node]
recalcexpressions = False
for attrname in attribmap[node]:
@@ -3129,6 +3245,29 @@ def get_globals():
bkupglobals[globvar] = _cfgstore['globals'][globvar]
return bkupglobals
def _init_indexes():
maxidx = get_global('max_node_index')
if maxidx is not None or 'main' not in _cfgstore:
return
maxidx = 1
maincfgstore = _cfgstore['main']
nodes_without_index = []
for node in confluent.util.natural_sort(maincfgstore.get('nodes', {})):
nidx = maincfgstore['nodes'][node].get('id.index', {}).get('value', None)
if nidx is not None:
if nidx >= maxidx:
maxidx = nidx + 1
else:
nodes_without_index.append(node)
for node in nodes_without_index:
maincfgstore['nodes'][node]['id.index'] = {'value': maxidx}
maxidx += 1
_mark_dirtykey('nodes', node, None)
set_global('max_node_index', maxidx)
set_global('free_node_indexes', [])
ConfigManager._bg_sync_to_file()
def init(stateless=False):
global _cfgstore
global _ready
@@ -3141,6 +3280,7 @@ def init(stateless=False):
_cfgstore = {}
members = list(list_collective())
if len(members) < 2:
_init_indexes()
_ready = True
+24 -2
View File
@@ -76,7 +76,7 @@ import shutil
vinz = None
pluginmap = {}
dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
dispatch_plugins = (b'remoteconfig', b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol', b'geist', u'geist', b'deltapdu', u'deltapdu', b'eatonpdu', u'eatonpdu', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
PluginCollection = plugin.PluginCollection
@@ -300,6 +300,20 @@ def _init_core():
'default': 'ipmi',
}),
},
'certificate': {
'sign': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'generate_csr': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'install': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'certificate_authorities': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -462,7 +476,15 @@ def _init_core():
}),
'ident_image': PluginRoute({
'handler': 'identimage'
})
}),
'remote_config': {
'run': PluginRoute({
'handler': 'remoteconfig'
}),
'active': PluginCollection({
'handler': 'remoteconfig'
}),
},
},
'events': {
'hardware': {
+43 -11
View File
@@ -98,6 +98,7 @@ import eventlet
import eventlet.greenpool
import eventlet.semaphore
autosensors = set()
scanner = None
@@ -106,6 +107,11 @@ try:
except NameError:
unicode = str
try:
import cryptography.x509.verification as verification
except ImportError:
verification = None
class nesteddict(dict):
def __missing__(self, key):
@@ -1059,8 +1065,12 @@ def get_nodename_sysdisco(cfg, handler, info):
return currnode
else:
baynum = info['bay']
nl = cfg.filter_node_attributes('enclosure.manager=' + currnode)
nl = list(cfg.filter_node_attributes('enclosure.bay={0}'.format(baynum), nl))
alphabaynum = '{}{}'.format((int(baynum) + 1) // 2, 'ab'[(int(baynum) - 1) % 2])
onl = cfg.filter_node_attributes('enclosure.manager=' + currnode)
nl = list(cfg.filter_node_attributes('enclosure.bay={}'.format(baynum), onl))
if len(nl) == 1:
return nl[0]
nl = list(cfg.filter_node_attributes('enclosure.bay={}'.format(alphabaynum), onl))
if len(nl) == 1:
return nl[0]
@@ -1182,14 +1192,21 @@ def get_nodename_from_enclosures(cfg, info):
encl = nodes_by_uuid[cuuid]
bay = info.get('enclosure.bay', None)
if bay:
tnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
otnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
tnl = list(
cfg.filter_node_attributes('enclosure.bay={0}'.format(bay),
tnl))
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), otnl))
if len(tnl) == 1:
# This is not a secure assurance, because it's by
# uuid instead of a key
nodename = tnl[0]
else:
alphabay = '{}{}'.format((int(bay) + 1) // 2, 'ab'[(int(bay) - 1) % 2])
tnl = list(
cfg.filter_node_attributes('enclosure.bay={}'.format(alphabay), otnl))
if len(tnl) == 1:
# Fallback alpha-bay mapping resolved to a single node
nodename = tnl[0]
return nodename
@@ -1215,10 +1232,15 @@ def search_smms_by_cert(currsmm, cert, cfg):
port = neigh.get('port', None)
if port is not None:
bay = port + 1
nl = list(
onl = list(
cfg.filter_node_attributes('enclosure.manager=' + currsmm))
nl = list(
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), nl))
cfg.filter_node_attributes('enclosure.bay={}'.format(bay), onl))
if len(nl) == 1:
return currsmm, bay, nl[0]
alphabay = '{}{}'.format((bay + 1) // 2, 'ab'[(bay - 1) % 2])
nl = list(
cfg.filter_node_attributes('enclosure.bay={}'.format(alphabay), onl))
if len(nl) == 1:
return currsmm, bay, nl[0]
return currsmm, bay, None
@@ -1311,8 +1333,15 @@ def eval_node(cfg, handler, info, nodename, manual=False):
return
# search for nodes fitting our description using filters
# lead with the most specific to have a small second pass
nl = list(cfg.filter_node_attributes(
'enclosure.bay={0}'.format(info['enclosure.bay']), nl))
baynum = info.get('enclosure.bay', None)
if baynum:
nnl = list(cfg.filter_node_attributes(
'enclosure.bay={}'.format(baynum), nl))
if len(nnl) == 0:
alphabaynum = '{}{}'.format((int(baynum) + 1) // 2, 'ab'[(int(baynum) - 1) % 2])
nnl = list(cfg.filter_node_attributes(
'enclosure.bay={}'.format(alphabaynum), nl))
nl = nnl
if len(nl) != 1:
info['discofailure'] = 'ambigconfig'
if len(nl):
@@ -1323,7 +1352,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
errorstr = 'The {0} in enclosure {1} bay {2} does not ' \
'seem to be a defined node ({3})'.format(
handler.devname, nodename,
info['enclosure.bay'],
baynum,
handler.ipaddr,
)
if manual:
@@ -1472,7 +1501,7 @@ def discover_node(cfg, handler, info, nodename, manual):
break
log.log({'info': 'Discovered {0} ({1})'.format(nodename,
handler.devname)})
if nodeconfig:
if nodeconfig or handler.current_cert_self_signed():
bmcaddr = cfg.get_node_attributes(nodename, 'hardwaremanagement.manager')
bmcaddr = bmcaddr.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', '')
if not bmcaddr:
@@ -1481,9 +1510,12 @@ def discover_node(cfg, handler, info, nodename, manual):
bmcaddr = bmcaddr.split('/', 1)[0]
wait_for_connection(bmcaddr)
socket.getaddrinfo(bmcaddr, 443)
if nodeconfig:
subprocess.check_call(['/opt/confluent/bin/nodeconfig', nodename] + nodeconfig)
log.log({'info': 'Configured {0} ({1})'.format(nodename,
handler.devname)})
if verification and handler.current_cert_self_signed():
handler.autosign_certificate()
info['discostatus'] = 'discovered'
for i in pending_by_uuid.get(curruuid, []):
@@ -17,6 +17,10 @@ import errno
import eventlet
import socket
webclient = eventlet.import_patched('pyghmi.util.webclient')
try:
import cryptography.x509 as x509
except ImportError:
x509 = None
class NodeHandler(object):
https_supported = True
@@ -59,6 +63,40 @@ class NodeHandler(object):
# may occur against the target in a short while
return True
def current_cert_self_signed(self):
if not x509:
return
if not self._ipaddr:
return
try:
wc = webclient.SecureHTTPConnection(self._ipaddr, verifycallback=self._savecert, port=443)
wc.connect()
wc.close()
if not self._fp:
return False
# Check if certificate is self-signed by comparing issuer and subject
cert = self._fp
certobj = x509.load_der_x509_certificate(cert)
skid = None
akid = None
for ext in certobj.extensions:
if ext.oid == x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER:
skid = ext.value
elif ext.oid == x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER:
akid = ext.value
if akid:
if skid.digest == akid.key_identifier:
return True
elif certobj.issuer == certobj.subject:
return True
except Exception:
pass
return False
def autosign_certificate(self):
# A no-op by default
return
def scan(self):
# Do completely passive things to enhance data.
# Probe is permitted to for example attempt a login
@@ -23,6 +23,7 @@ try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import eventlet.green.subprocess as subprocess
getaddrinfo = eventlet.support.greendns.getaddrinfo
@@ -326,6 +327,14 @@ class NodeHandler(generic.NodeHandler):
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
def autosign_certificate(self):
nodename = self.nodename
hwmgt_method = self.configmanager.get_node_attributes(
nodename, 'hardwaremanagement.method').get(
nodename, {}).get('hardwaremanagement.method', {}).get('value', 'ipmi')
if hwmgt_method != 'redfish':
return
subprocess.check_call(['/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47'])
def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
@@ -29,6 +29,7 @@ import eventlet.green.socket as socket
webclient = eventlet.import_patched('pyghmi.util.webclient')
import struct
getaddrinfo = eventlet.support.greendns.getaddrinfo
import eventlet.green.subprocess as subprocess
def fixuuid(baduuid):
@@ -704,6 +705,15 @@ class NodeHandler(immhandler.NodeHandler):
if em:
self.configmanager.set_node_attributes(
{em: {'id.uuid': enclosureuuid}})
def autosign_certificate(self):
nodename = self.nodename
hwmgt_method = self.configmanager.get_node_attributes(
nodename, 'hardwaremanagement.method').get(
nodename, {}).get('hardwaremanagement.method', {}).get('value', 'ipmi')
if hwmgt_method != 'redfish':
return
subprocess.check_call(['/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47'])
def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
@@ -441,7 +441,7 @@ def _find_service(service, target):
continue
else:
for targurl in peerdata[nid]['urls']:
if '/eth' in targurl and targurl.endswith('.xml'):
if targurl and targurl.endswith('.xml'):
pooltargs.append(('/redfish/v1/', peerdata[nid], 'megarac-bmc'))
# For now, don't interrogate generic redfish bmcs
# This is due to a need to deduplicate from some supported SLP
@@ -491,7 +491,7 @@ def check_fish(urldata, port=443, verifycallback=None):
data['services'] = ['lenovo-xcc'] if 'xcc-variant' not in peerinfo else ['lenovo-xcc' + peerinfo['xcc-variant']]
return data
except (IndexError, KeyError):
if 'type' in peerinfo and peerinfo['type'].lower() == 'lenovo-smm3':
if 'type' in peerinfo and peerinfo['type'].lower() in ('lenovo-smm3', 'smm3'):
del peerinfo['xcc-variant']
data['uuid'] = peerinfo['enclosure-uuid']
data['services'] = ['lenovo-smm3']
+14 -4
View File
@@ -17,6 +17,7 @@
# This SCGI server provides a http wrap to confluent api
# It additionally manages httprequest console sessions
import base64
import shutil
try:
import Cookie
except ModuleNotFoundError:
@@ -1217,7 +1218,7 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
rspdata, sort_keys=True, indent=4, ensure_ascii=False).encode('utf-8'))
def serve(bind_host, bind_port):
def serve(bind_host, bind_port, bind_group=None, bind_perms=None):
# TODO(jbjohnso): move to unix socket and explore
# either making apache deal with it
# or just supporting nginx or lighthttpd
@@ -1232,13 +1233,17 @@ def serve(bind_host, bind_port):
while not sock:
try:
if '/' in bind_host:
oldumask = os.umask(0o777 - bind_perms)
try:
os.remove(bind_host)
except Exception:
pass
sock = eventlet.listen(
bind_host, family=socket.AF_UNIX)
os.chmod(bind_host, 0o666)
os.umask(oldumask)
os.chmod(bind_host, bind_perms)
if bind_group:
shutil.chown(bind_host, group=bind_group)
else:
addrinfo = socket.getaddrinfo(bind_host, bind_port)[0]
sock = eventlet.listen(
@@ -1264,17 +1269,22 @@ def serve(bind_host, bind_port):
class HttpApi(object):
def __init__(self, bind_host=None, bind_port=None):
def __init__(self, bind_host=None, bind_port=None, bind_group=None, bind_perms=None):
self.server = None
self.bind_host = bind_host or '127.0.0.1'
self.bind_port = bind_port or 4005
# Ultimately, a unix socket is being used in lieu of a TCP socket,
# so open permissions make sense as the security is not based solely on socket access
# however, steering it to webserver group can be done for extra confidence
self.bind_group = bind_group
self.bind_perms = bind_perms or 0o666
def start(self):
global auditlog
global tracelog
tracelog = log.Logger('trace')
auditlog = log.Logger('audit')
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port)
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port, self.bind_group, self.bind_perms)
_cleaner = eventlet.spawn(_sessioncleaner)
+22 -5
View File
@@ -314,14 +314,14 @@ def run(args):
auth.check_for_yaml()
collective.startup()
consoleserver.initialize()
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
http_bind_host, http_bind_port, http_bind_group, http_bind_perms = _get_connector_config('http')
sock_bind_host, sock_bind_port, sock_bind_group, sock_bind_perms = _get_connector_config('socket')
try:
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port)
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port, sock_bind_group, sock_bind_perms)
sockservice.start()
except NameError:
pass
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
webservice = httpapi.HttpApi(http_bind_host, http_bind_port, http_bind_group, http_bind_perms)
webservice.start()
while len(list(configmanager.list_collective())) >= 2:
# If in a collective, stall automatic startup activity
@@ -340,7 +340,24 @@ def run(args):
def _get_connector_config(session):
host = conf.get_option(session, 'bindhost')
port = conf.get_int_option(session, 'bindport')
return (host, port)
group = conf.get_option(session, 'bindgroup')
perms = conf.get_option(session, 'bindperms')
if perms:
if perms.startswith('0'):
perms = int(perms, 8)
else:
# Parse rw-rw-rw- format (user, group, other)
perms_value = 0
perm_map = {'r': 4, 'w': 2, 'x': 1}
for i, section in enumerate([perms[0:3], perms[3:6], perms[6:9]]):
for char in section:
if char in perm_map:
perms_value += perm_map[char] * (8 ** (2 - i))
perms = perms_value
else:
perms = None
return (host, port, group, perms)
def _get_logdirectory():
return conf.get_option('globals', 'logdirectory')
+26
View File
@@ -519,6 +519,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputAlertDestination(path, nodes, inputdata, multinode)
elif len(path) == 3 and path[:3] == ['configuration', 'management_controller', 'certificate_authorities'] and operation not in ('retrieve', 'delete'):
return InputCertificateAuthority(path, nodes, inputdata)
elif len(path) == 4 and path[:4] == ['configuration', 'management_controller', 'certificate', 'sign'] and operation not in ('retrieve', 'delete'):
return InputSigningParameters(path, inputdata, nodes, configmanager)
elif path == ['identify'] and operation != 'retrieve':
return InputIdentifyMessage(path, nodes, inputdata)
elif path == ['events', 'hardware', 'decode']:
@@ -578,6 +580,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputLicense(path, nodes, inputdata, configmanager)
elif path == ['deployment', 'lock'] and inputdata:
return InputDeploymentLock(path, nodes, inputdata)
elif path == ['deployment', 'remote_config', 'run'] and inputdata:
return InputRemoteConfig(path, nodes, inputdata)
elif path == ['deployment', 'ident_image']:
return InputIdentImage(path, nodes, inputdata)
elif path == ['console', 'ikvm']:
@@ -721,6 +725,9 @@ class InputConfigChangeSet(InputExpression):
endattrs = {}
for attr in attrs:
origval = attrs[attr]
if isinstance(origval, int):
endattrs[attr] = origval
continue
if isinstance(origval, bytes) or isinstance(origval, unicode):
origval = {'expression': origval}
if 'expression' not in origval:
@@ -956,6 +963,20 @@ class ConfluentInputMessage(ConfluentMessage):
def is_valid_key(self, key):
return key in self.valid_values
class InputSigningParameters(InputConfigChangeSet):
def get_days(self, node):
attribs = self.get_attributes(node)
return int(attribs['days'])
def get_added_names(self, node):
attribs = self.get_attributes(node)
addnames = []
for subj in (attribs.get('added_names') or '').split(','):
if subj:
addnames.append(subj.strip())
return addnames
class InputCertificateAuthority(ConfluentInputMessage):
keyname = 'pem'
@@ -975,6 +996,10 @@ class InputDeploymentLock(ConfluentInputMessage):
keyname = 'lock'
valid_values = ['autolock', 'unlocked', 'locked']
class InputRemoteConfig(ConfluentInputMessage):
keyname = 'category'
valid_values = ['post.d', 'firstboot.d', 'onboot.d']
class DeploymentLock(ConfluentChoiceMessage):
valid_values = set([
'autolock',
@@ -1703,6 +1728,7 @@ class Disk(ConfluentMessage):
state_aliases = {
'unconfigured bad': 'fault',
'unconfigured good': 'unconfigured',
'unconfiguredgood': 'unconfigured',
'global hot spare': 'hotspare',
'dedicated hot spare': 'hotspare',
}
+35 -4
View File
@@ -35,6 +35,7 @@ if __name__ == '__main__':
import confluent.config.configmanager as cfm
import base64
import confluent.networking.nxapi as nxapi
import confluent.networking.srlinux as srlinux
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages as msg
@@ -189,6 +190,10 @@ def detect_backend(switch, verifier):
apicheck, retcode = wc.grab_json_response_with_status('/api/')
if retcode == 400 and apicheck.startswith(b'{"imdata":['):
_fastbackends[switch] = 'nxapi'
else:
rsp = wc.grab_json_response_with_status('/jsonrpc', {'dummy': 'data'}, returnheaders=True)
if rsp[1] == 401 and rsp[2].get('WWW-Authenticate', '').startswith('Basic realm="SRLinux"'):
_fastbackends[switch] = 'srlinux'
return _fastbackends.get(switch, None)
def _extract_neighbor_data_https(switch, user, password, cfm, lldpdata):
@@ -203,6 +208,8 @@ def _extract_neighbor_data_https(switch, user, password, cfm, lldpdata):
return _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc)
elif backend == 'nxapi':
return _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc)
elif backend == 'srlinux':
return _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc)
@@ -217,10 +224,28 @@ def _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc):
)
portdata['peerid'] = peerid
_extract_extended_desc(portdata, portdata['peerdescription'], True)
portdata['switch'] = switch
_neighbypeerid[peerid] = portdata
lldpdata[port] = portdata
_neighdata[switch] = lldpdata
def _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc):
cli = srlinux.SRLinuxClient(switch, user, password, cfm)
lldpinfo = cli.get_lldp()
for port in lldpinfo:
portdata = lldpinfo[port]
peerid = '{0}.{1}'.format(
portdata.get('peerchassisid', '').replace(':', '-').replace('/', '-'),
portdata.get('peerportid', '').replace(':', '-').replace('/', '-'),
)
portdata['peerid'] = peerid
_extract_extended_desc(portdata, portdata['peerdescription'], True)
portdata['switch'] = switch
_neighbypeerid[peerid] = portdata
lldpdata[port] = portdata
_neighdata[switch] = lldpdata
def _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc):
wc.set_basic_credentials(user, password)
neighdata = wc.grab_json_response('/affluent/lldp/all')
@@ -255,7 +280,13 @@ def _extract_neighbor_data_b(args):
args are carried as a tuple, because of eventlet convenience
"""
switch, password, user, cfm, force = args[:5]
# Safely unpack args with defaults to avoid IndexError
switch = args[0] if len(args) > 0 else None
password = args[1] if len(args) > 1 else None
user = args[2] if len(args) > 2 else None
cfm = args[3] if len(args) > 3 else None
privproto = args[4] if len(args) > 4 else None
force = args[5] if len(args) > 5 else False
vintage = _neighdata.get(switch, {}).get('!!vintage', 0)
now = util.monotonic_time()
if vintage > (now - 60) and not force:
@@ -265,7 +296,7 @@ def _extract_neighbor_data_b(args):
return _extract_neighbor_data_https(switch, user, password, cfm, lldpdata)
except Exception as e:
pass
conn = snmp.Session(switch, password, user)
conn = snmp.Session(switch, password, user, privacy_protocol=privproto)
sid = None
for sysid in conn.walk('1.3.6.1.2.1.1.2'):
sid = str(sysid[1][6:])
@@ -364,8 +395,8 @@ def _extract_neighbor_data(args):
return _extract_neighbor_data_b(args)
except Exception as e:
yieldexc = False
if len(args) >= 6:
yieldexc = args[5]
if len(args) >= 7:
yieldexc = args[6]
if yieldexc:
return e
else:
+22 -14
View File
@@ -55,6 +55,7 @@ import confluent.log as log
import confluent.messages as msg
import confluent.noderange as noderange
import confluent.networking.nxapi as nxapi
import confluent.networking.srlinux as srlinux
import confluent.util as util
from eventlet.greenpool import GreenPool
import eventlet.green.subprocess as subprocess
@@ -153,7 +154,7 @@ def _nodelookup(switch, ifname):
return None
def _fast_map_switch(args):
switch, password, user, cfgm = args
switch, password, user, cfgm = args[:4]
macdata = None
kv = util.TLSCertVerifier(cfgm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
@@ -162,8 +163,16 @@ def _fast_map_switch(args):
return _affluent_map_switch(switch, password, user, cfgm, macdata)
elif backend == 'nxapi':
return _nxapi_map_switch(switch, password, user, cfgm)
elif backend == 'srlinux':
return _srlinux_map_switch(switch, password, user, cfgm)
raise Exception("No fast backend match")
def _srlinux_map_switch(switch, password, user, cfgm):
cli = srlinux.SRLinuxClient(switch, user, password, cfgm)
mt = cli.get_mac_table()
_macsbyswitch[switch] = mt
_fast_backend_fixup(mt, switch)
def _nxapi_map_switch(switch, password, user, cfgm):
cli = nxapi.NxApiClient(switch, user, password, cfgm)
mt = cli.get_mac_table()
@@ -213,14 +222,14 @@ def _fast_backend_fixup(macs, switch):
else:
_nodesbymac[mac] = (nodename, nummacs)
def _offload_map_switch(switch, password, user):
def _offload_map_switch(switch, password, user, privprotocol=None):
if _offloader is None:
_start_offloader()
evtid = random.randint(0, 4294967295)
while evtid in _offloadevts:
evtid = random.randint(0, 4294967295)
_offloadevts[evtid] = eventlet.Event()
_offloader.stdin.write(msgpack.packb((evtid, switch, password, user),
_offloader.stdin.write(msgpack.packb((evtid, switch, password, user, privprotocol),
use_bin_type=True))
_offloader.stdin.flush()
result = _offloadevts[evtid].wait()
@@ -280,12 +289,11 @@ def _map_switch_backend(args):
# fallback if ifName is empty
#
global _macmap
if len(args) == 4:
switch, password, user, _ = args # 4th arg is for affluent only
if not user:
user = None
else:
switch, password = args
switch = args[0] if len(args) > 0 else None
password = args[1] if len(args) > 1 else None
user = args[2] if len(args) > 2 else None
privprotocol = args[4] if len(args) > 4 else None
if not user: # make '' be treated as None
user = None
if switch not in noaffluent:
try:
@@ -298,7 +306,7 @@ def _map_switch_backend(args):
except Exception as e:
pass
mactobridge, ifnamemap, bridgetoifmap = _offload_map_switch(
switch, password, user)
switch, password, user, privprotocol)
maccounts = {}
bridgetoifvalid = False
for mac in mactobridge:
@@ -367,9 +375,9 @@ def _map_switch_backend(args):
_nodesbymac[mac] = (nodename, maccounts[ifname])
_macsbyswitch[switch] = newmacs
def _snmp_map_switch_relay(rqid, switch, password, user):
def _snmp_map_switch_relay(rqid, switch, password, user, privprotocol=None):
try:
res = _snmp_map_switch(switch, password, user)
res = _snmp_map_switch(switch, password, user, privprotocol)
payload = msgpack.packb((rqid,) + res, use_bin_type=True)
try:
sys.stdout.buffer.write(payload)
@@ -391,10 +399,10 @@ def _snmp_map_switch_relay(rqid, switch, password, user):
finally:
sys.stdout.flush()
def _snmp_map_switch(switch, password, user):
def _snmp_map_switch(switch, password, user, privprotocol=None):
haveqbridge = False
mactobridge = {}
conn = snmp.Session(switch, password, user)
conn = snmp.Session(switch, password, user, privacy_protocol=privprotocol)
ifnamemap = get_portnamemap(conn)
for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
haveqbridge = True
@@ -21,7 +21,7 @@ import confluent.collective.manager as collective
def get_switchcreds(configmanager, switches):
switchcfg = configmanager.get_node_attributes(
switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity',
'secret.hardwaremanagementpassword',
'secret.hardwaremanagementpassword', 'snmp.privacyprotocol',
'collective.managercandidates'), decrypt=True)
switchauth = []
for switch in switches:
@@ -39,6 +39,7 @@ def get_switchcreds(configmanager, switches):
user = None
password = switchparms.get(
'secret.snmpcommunity', {}).get('value', None)
privacy_protocol = None
if not password:
password = switchparms.get(
'secret.hardwaremanagementpassword', {}).get('value',
@@ -47,7 +48,9 @@ def get_switchcreds(configmanager, switches):
'secret.hardwaremanagementuser', {}).get('value', None)
if not user:
user = None
switchauth.append((switch, password, user, configmanager))
privacy_protocol = switchparms.get(
'snmp.privacyprotocol', {}).get('value', None)
switchauth.append((switch, password, user, configmanager, privacy_protocol))
return switchauth
@@ -0,0 +1,252 @@
import confluent.util as util
import eventlet
webclient = eventlet.import_patched('pyghmi.util.webclient')
class SRLinuxClient:
def __init__(self, switch, user, password, configmanager):
self.cachedurls = {}
self.switch = switch
if configmanager:
cv = util.TLSCertVerifier(
configmanager, switch, 'pubkeys.tls_hardwaremanager'
).verify_cert
else:
cv = lambda x: True
self.user = user
self.password = password
try:
self.user = self.user.decode()
self.password = self.password.decode()
except Exception:
pass
self.wc = webclient.SecureHTTPConnection(switch, port=443, verifycallback=cv)
self.wc.set_basic_credentials(self.user, self.password)
self.rpc_id = 1
self.login()
def login(self):
# Just a quick query to validate that credentials are correct and device is reachable and TLS works out however it is supposed to
self._get_state('/system/information')
def _rpc_call(self, method, params=None):
"""Make a JSON-RPC call to SR-Linux"""
payload = {
'jsonrpc': '2.0',
'id': self.rpc_id,
'method': method,
}
if params:
payload['params'] = params
self.rpc_id += 1
rsp = self.wc.grab_json_response_with_status('/jsonrpc', payload)
if rsp[1] != 200:
raise Exception(f"Failed RPC call: {method}, status: {rsp[1]}")
result = rsp[0]
if 'error' in result:
raise Exception(f"RPC error: {result['error']}")
return result.get('result', {})
def _get_state(self, path, datastore='state'):
"""Get state data from SR-Linux using JSON-RPC get method"""
params = {
'commands': [
{
'path': path,
'datastore': datastore
}
]
}
result = self._rpc_call('get', params)
return result
def get_firmware(self):
"""Get firmware/software version information"""
firmdata = {}
result = self._get_state('/system/information')
for item in result:
if 'version' in item:
firmdata['SR-Linux'] = {'version': item['version']}
if 'build-date' in item:
if 'SR-Linux' in firmdata:
firmdata['SR-Linux']['date'] = item['build-date']
return firmdata
def get_sensors(self):
"""Get sensor readings from the device"""
sensedata = []
result = self._get_state('/platform/control/temperature')
for item in result:
for pcc in item:
currreading = {}
for reading in item[pcc]:
if reading.get('temperature', {}).get('alarm-status', False):
currreading['health'] = 'critical'
else:
currreading['health'] = 'ok'
states = []
if reading.get('oper-state', 'up',) != 'up':
states = [reading.get('oper-reason', 'unknown')]
currreading['states'] = states
currreading['name'] = 'Slot {} Temperature'.format(reading.get('slot', 'Unknown'))
currreading['value'] = reading.get('temperature', {}).get('instant', 'Unknown')
currreading['units'] = '°C'
sensedata.append(currreading)
result = self._get_state('/platform/fan-tray')
for item in result:
for pft in item:
currreading = {}
for reading in item[pft]:
if reading.get('srl_nokia-platform-healthz:healthz', {}).get('status', 'healthy') != 'healthy':
currreading['health'] = 'critical'
else:
currreading['health'] = 'ok'
states = []
if reading.get('oper-state', 'up',) != 'up':
states = [reading.get('oper-reason', 'unknown')]
currreading['states'] = states
currreading['name'] = 'Fan Tray {}'.format(reading.get('id', 'Unknown'))
currreading['value'] = reading.get('fan', {}).get('speed', 'Unknown')
currreading['units'] = '%'
sensedata.append(currreading)
result = self._get_state('/platform/power-supply')
for item in result:
for pps in item:
for reading in item[pps]:
currreading = {}
if reading.get('srl_nokia-platform-healthz:healthz', {}).get('status', 'healthy') != 'healthy':
currreading['health'] = 'critical'
else:
currreading['health'] = 'ok'
states = []
if reading.get('oper-state', 'up',) != 'up':
states = [reading.get('oper-reason', 'unknown')]
currreading['states'] = states
currreading['name'] = 'Power Supply {} Health'.format(reading.get('id', 'Unknown'))
sensedata.append(currreading)
tempreading = {'health': 'ok'}
tempreading['name'] = 'Power Supply {} Temperature'.format(reading.get('id', 'Unknown'))
tempreading['value'] = reading.get('temperature', {}).get('instant', 'Unknown')
tempreading['units'] = '°C'
sensedata.append(tempreading)
for powstat in 'current', 'power', 'voltage':
powreading = {'health': 'ok'}
powreading['name'] = 'Power Supply {} {}'.format(reading.get('id', 'Unknown'), powstat.capitalize())
powreading['value'] = reading.get('input', {}).get(powstat, 'Unknown')
if powstat == 'current':
powreading['units'] = 'A'
elif powstat == 'power':
powreading['units'] = 'W'
elif powstat == 'voltage':
powreading['units'] = 'V'
sensedata.append(powreading)
return sensedata
def get_health(self):
healthdata = {'health': 'ok', 'sensors': []}
sensors = self.get_sensors()
for sensor in sensors:
currhealth = sensor.get('health', 'ok')
if currhealth != 'ok':
healthdata['sensors'].append(sensor)
if sensor['health'] == 'critical':
healthdata['health'] = 'critical'
elif sensor['health'] == 'warning' and healthdata['health'] != 'critical':
healthdata['health'] = 'warning'
return healthdata
def get_inventory(self):
invdata = []
results = self._get_state('/platform/chassis')
for result in results:
invinfo = {'name': 'System', 'present': True}
invinfo['information'] = {'Manufacturer': 'Nokia'}
if isinstance(result, dict):
for key, value in result.items():
if key == 'serial-number':
invinfo['information']['Serial Number'] = value
elif key == 'part-number':
invinfo['information']['Part Number'] = value
elif key == 'type':
invinfo['information']['Model'] = value
if invinfo['information']:
invdata.append(invinfo)
return invdata
def get_mac_table(self):
macdict = {}
response = self._get_state('/network-instance/bridge-table/mac-table/mac')
for datum in response:
for niname in datum:
for nin in datum[niname]:
btable = nin.get('bridge-table', {})
for btab in btable:
macs = btable[btab].get('mac', [])
for macent in macs:
macaddr = macent.get('address', None)
if macaddr:
macport = macent.get('destination', None)
if macport:
macdict.setdefault(macport, []).append(macaddr)
return macdict
def get_lldp(self):
lldpbyport = {}
response = self._get_state('/system/lldp/interface')
for datum in response:
for intfname in datum:
lldpallinfo = datum[intfname]
for lldpdatum in lldpallinfo:
myportname = lldpdatum.get('name', None)
for neighinfo in lldpdatum.get('neighbor', []):
peerdesc = neighinfo.get('system-description', 'Unknown')
peername = neighinfo.get('system-name', 'Unknown')
peerchassisid = neighinfo.get('chassis-id', 'Unknown')
peerportid = neighinfo.get('port-id', 'Unknown')
lldpinfo = {
'verified': True, # Data provided with authentication over TLS
'peerdescription': peerdesc,
'peername': peername,
'peerchassisid': peerchassisid,
'peerportid': peerportid,
'portid': myportname,
'port': myportname,
}
lldpbyport[myportname] = lldpinfo
return lldpbyport
if __name__ == '__main__':
import sys
import os
from pprint import pprint
myuser = os.environ.get('SWITCHUSER')
mypass = os.environ.get('SWITCHPASS')
if not myuser or not mypass:
print("Set SWITCHUSER and SWITCHPASS environment variables")
sys.exit(1)
srl = SRLinuxClient(sys.argv[1], myuser, mypass, None)
pprint(srl.get_firmware())
pprint(srl.get_inventory())
pprint(srl.get_sensors())
pprint(srl.get_health())
pprint(srl.get_lldp())
pprint(srl.get_mac_table())
+2
View File
@@ -1137,6 +1137,8 @@ def generate_stock_profiles(defprofile, distpath, targpath, osname,
initrds = ['{0}/initramfs/{1}'.format(defprofile, initrd) for initrd in os.listdir('{0}/initramfs'.format(defprofile))]
if os.path.exists('{0}/initramfs/{1}'.format(defprofile, arch)):
initrds.extend(['{0}/initramfs/{1}/{2}'.format(defprofile, arch, initrd) for initrd in os.listdir('{0}/initramfs/{1}'.format(defprofile, arch))])
elif arch == 'arm64' and os.path.exists('{0}/initramfs/aarch64'.format(defprofile)):
initrds.extend(['{0}/initramfs/aarch64/{1}'.format(defprofile, initrd) for initrd in os.listdir('{0}/initramfs/aarch64'.format(defprofile))])
for fullpath in initrds:
initrd = os.path.basename(fullpath)
if os.path.isdir(fullpath):
@@ -0,0 +1,38 @@
import confluent.selfservice as selfservice
import confluent.messages as msg
import confluent.runansible as runansible
_user_initiated_runs = {}
def update(nodes, element, configmanager, inputdata):
if element[-1] != 'run':
raise ValueError('Invalid element for remoteconfig plugin')
for node in nodes:
category = inputdata.inputbynode[node]
playlist = selfservice.list_ansible_scripts(configmanager, node, category)
if playlist:
_user_initiated_runs[node] = True
runansible.run_playbooks(playlist, [node])
yield msg.CreatedResource(
'/nodes/{0}/deployment/remote_config/active/{0}'.format(node))
else:
yield msg.ConfluentNodeError('No remote configuration for category "{0}"', node)
def retrieve(nodes, element, configmanager, inputdata):
for node in nodes:
if element[-1] == 'active':
rst = runansible.running_status.get(node, None)
if not rst:
return
yield msg.ChildCollection(node)
elif element[-2] == 'active' and element[-1] == node:
rst = runansible.running_status.get(node, None)
if not rst:
return
playstatus = rst.dump_dict()
if playstatus['complete'] and _user_initiated_runs.get(node, False):
del runansible.running_status[node]
del _user_initiated_runs[node]
yield msg.KeyValueData(playstatus, node)
@@ -20,6 +20,7 @@ import confluent.messages as msg
import confluent.util as util
import copy
import errno
from confluent import certutil
import eventlet
import eventlet.event
import eventlet.green.threading as threading
@@ -37,6 +38,7 @@ ipmicommand = eventlet.import_patched('pyghmi.redfish.command')
import socket
import ssl
import traceback
import tempfile
if not hasattr(ssl, 'SSLEOFError'):
ssl.SSLEOFError = None
@@ -184,8 +186,12 @@ class IpmiCommandWrapper(ipmicommand.Command):
(node,), ('secret.hardwaremanagementuser', 'collective.manager',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager'), self._attribschanged)
htn = cfm.get_node_attributes(node, 'hardwaremanagement.manager_tls_name')
subject = htn.get(node, {}).get('hardwaremanagement.manager_tls_name', {}).get('value', None)
if not subject:
subject = kwargs['bmc']
kv = util.TLSCertVerifier(cfm, node,
'pubkeys.tls_hardwaremanager').verify_cert
'pubkeys.tls_hardwaremanager', subject).verify_cert
kwargs['verifycallback'] = kv
try:
super(IpmiCommandWrapper, self).__init__(**kwargs)
@@ -528,6 +534,8 @@ class IpmiHandler(object):
return self.handle_alerts()
elif self.element[1:3] == ['management_controller', 'certificate_authorities']:
return self.handle_cert_authorities()
elif self.element[1:3] == ['management_controller', 'certificate']:
return self.handle_certificate()
elif self.element[1:3] == ['management_controller', 'users']:
return self.handle_users()
elif self.element[1:3] == ['management_controller', 'net_interfaces']:
@@ -578,6 +586,26 @@ class IpmiHandler(object):
self.pyghmi_event_to_confluent(event)
self.output.put(msg.EventCollection((event,), name=self.node))
def handle_certificate(self):
self.element = self.element[3:]
if len(self.element) != 1:
raise Exception('Not implemented')
if self.element[0] == 'sign' and self.op == 'update':
csr = self.ipmicmd.get_bmc_csr()
subj, san = util.get_bmc_subject_san(self.cfm, self.node, self.inputdata.get_added_names(self.node))
with tempfile.NamedTemporaryFile() as tmpfile:
tmpfile.write(csr.encode())
tmpfile.flush()
certfile = tempfile.NamedTemporaryFile(delete=False)
certname = certfile.name
certfile.close()
certutil.create_certificate(None, certname, tmpfile.name, subj, san, backdate=False,
days=self.inputdata.get_days(self.node))
with open(certname, 'rb') as certf:
cert = certf.read()
os.unlink(certname)
self.ipmicmd.install_bmc_certificate(cert)
def handle_cert_authorities(self):
if len(self.element) == 3:
if self.op == 'read':
@@ -0,0 +1,99 @@
import confluent.exceptions as exc
import confluent.networking.srlinux as srlinux
import eventlet
import eventlet.queue as queue
import confluent.messages as msg
def retrieve_node(node, element, user, pwd, configmanager, inputdata, results):
try:
retrieve_node_backend(node, element, user, pwd, configmanager, inputdata, results)
except exc.PubkeyInvalid as e:
results.put(msg.ConfluentNodeError(node, 'Mismatch detected between target certificate fingerprint '
'and pubkeys.tls_hardwaremanager attribute'))
except Exception as e:
results.put(e)
def simplify_name(name):
return name.lower().replace(' ', '_').replace('/', '-').replace(
'_-_', '-')
def retrieve_node_backend(node, element, user, pwd, configmanager, inputdata, results):
cli = srlinux.SRLinuxClient(node, user, pwd, configmanager)
if element == ['power', 'state']: # client initted successfully, must be on
results.put(msg.PowerState(node, 'on'))
elif element == ['health', 'hardware']:
hinfo = cli.get_health()
results.put(msg.HealthSummary(hinfo.get('health', 'unknown'), name=node))
results.put(msg.SensorReadings(hinfo.get('sensors', []), name=node))
elif element[:3] == ['inventory', 'hardware', 'all']:
if len(element) == 3:
results.put(msg.ChildCollection('all'))
return
invinfo = cli.get_inventory()
if invinfo:
results.put(msg.KeyValueData({'inventory': invinfo}, node))
elif element[:3] == ['inventory', 'firmware', 'all']:
if len(element) == 3:
results.put(msg.ChildCollection('all'))
return
fwinfo = []
for fwnam, fwdat in cli.get_firmware().items():
fwinfo.append({fwnam: fwdat})
if fwinfo:
results.put(msg.Firmware(fwinfo, node))
elif element == ['sensors', 'hardware', 'all']:
sensors = cli.get_sensors()
for sensor in sensors:
results.put(msg.ChildCollection(simplify_name(sensor['name'])))
elif element[:3] == ['sensors', 'hardware', 'all']:
sensors = cli.get_sensors()
for sensor in sensors:
if element[-1] == 'all' or simplify_name(sensor['name']) == element[-1]:
results.put(msg.SensorReadings([sensor], node))
else:
results.put(msg.ConfluentNodeError(node, 'Not supported'))
def retrieve(nodes, element, configmanager, inputdata):
results = queue.LightQueue()
workers = set([])
creds = configmanager.get_node_attributes(
nodes, ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True)
for node in nodes:
cred = creds.get(node, {})
user = cred.get('secret.hardwaremanagementuser', {}).get('value')
pwd = cred.get('secret.hardwaremanagementpassword', {}).get('value')
try:
user = user.decode()
pwd = pwd.decode()
except Exception:
pass
if not user or not pwd:
yield msg.ConfluentTargetInvalidCredentials(node)
continue
workers.add(eventlet.spawn(retrieve_node, node, element, user, pwd, configmanager, inputdata, results))
while workers:
try:
datum = results.get(block=True, timeout=10)
while datum:
if isinstance(datum, Exception):
raise datum
if datum:
yield datum
datum = results.get_nowait()
except queue.Empty:
pass
eventlet.sleep(0.001)
for t in list(workers):
if t.dead:
workers.discard(t)
try:
while True:
datum = results.get_nowait()
if datum:
yield datum
except queue.Empty:
pass
+74 -23
View File
@@ -21,12 +21,16 @@ try:
import eventlet.green.subprocess as subprocess
except ImportError:
pass
import base64
import eventlet.green.select as select
import shutil
import json
import socket
import msgpack
import os
import struct
import sys
import tempfile
anspypath = None
running_status = {}
@@ -38,6 +42,7 @@ class PlayRunner(object):
self.worker = None
self.results = []
self.complete = False
self.stdout = ''
def _start_playbooks(self):
self.worker = eventlet.spawn(self._really_run_playbooks)
@@ -49,6 +54,7 @@ class PlayRunner(object):
def dump_text(self):
stderr = self.stderr
stdout = self.stdout
retinfo = self.dump_dict()
textout = ''
for result in retinfo['results']:
@@ -65,6 +71,9 @@ class PlayRunner(object):
else:
textout += result['state'] + '\n'
textout += '\n'
if stdout:
textout += "OUTPUT **********************************\n"
textout += stdout
if stderr:
textout += "ERRORS **********************************\n"
textout += stderr
@@ -88,25 +97,58 @@ class PlayRunner(object):
if ansloc:
with open(ansloc, 'r') as onsop:
shebang = onsop.readline()
anspypath = shebang.strip().replace('#!', '')
anspypath = shebang.strip().replace('#!', '').strip()
mypath = anspypath
if not mypath:
mypath = sys.executable
with open(os.devnull, 'w+') as devnull:
targnodes = ','.join(self.nodes)
for playfilename in self.playfiles:
worker = subprocess.Popen(
[mypath, __file__, targnodes, playfilename],
stdin=devnull, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stder = worker.communicate()
self.stderr += stder.decode('utf8')
current = memoryview(stdout)
while len(current):
sz = struct.unpack('=q', current[:8])[0]
result = msgpack.unpackb(current[8:8+sz], raw=False)
self.results.append(result)
current = current[8+sz:]
with tempfile.TemporaryDirectory() as tmpdir:
feedback = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
suffix = base64.urlsafe_b64encode(os.urandom(6)).decode('ascii')
sockpath = os.path.join(tmpdir, 'feedback.sock.' + suffix)
localenv = os.environ.copy()
localenv['FEEDBACK_SOCK'] = sockpath
feedback.bind(sockpath)
feedback.listen(1)
with feedback:
with open(os.devnull, 'w+') as devnull:
targnodes = ','.join(self.nodes)
for playfilename in self.playfiles:
worker = subprocess.Popen(
[mypath, __file__, targnodes, playfilename],
stdin=devnull, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, env=localenv)
rlist, _, _ = select.select([feedback], [], [], 10)
if not rlist:
raise RuntimeError(
f"Timed out waiting for feedback socket connection for playbook '{playfilename}'"
)
def _recv_results(sock, timeout=0.1):
while select.select([sock], [], [], timeout)[0]:
hdr = b''
while len(hdr) < 8:
chunk = sock.recv(8 - len(hdr))
if not chunk:
if not hdr:
return
raise RuntimeError("Socket closed while receiving message header")
hdr += chunk
msglen = struct.unpack('=q', hdr)[0]
msg = b''
while len(msg) < msglen:
chunk = sock.recv(msglen - len(msg))
if not chunk:
raise RuntimeError("Socket closed while receiving message")
msg += chunk
self.results.append(msgpack.unpackb(msg, raw=False))
conn, _ = feedback.accept()
with conn:
while worker.poll() is None:
_recv_results(conn)
_recv_results(conn, timeout=0)
stdout, stder = worker.communicate()
self.stderr += stder.decode('utf8')
self.stdout += stdout.decode('utf8')
finally:
self.complete = True
@@ -119,7 +161,7 @@ def run_playbooks(playfiles, nodes):
runner._start_playbooks()
def print_result(result, state, collector=None):
def print_result(result, state, collector=None, callbacksock=None):
output = {
'task_name': result.task_name,
'changed': result._result.get('changed', ''),
@@ -130,12 +172,12 @@ def print_result(result, state, collector=None):
del result._result['warnings']
except KeyError:
pass
if collector:
if state != 'ok' and collector and hasattr(collector, '_dump_results'):
output['errorinfo'] = collector._dump_results(result._result)
msg = msgpack.packb(output, use_bin_type=True)
msglen = len(msg)
sys.stdout.buffer.write(struct.pack('=q', msglen))
sys.stdout.buffer.write(msg)
callbacksock.sendall(struct.pack('=q', msglen))
callbacksock.sendall(msg)
if __name__ == '__main__':
from ansible.inventory.manager import InventoryManager
@@ -149,16 +191,25 @@ if __name__ == '__main__':
import ansible.plugins.loader
import yaml
sockpath = os.environ.get('FEEDBACK_SOCK')
if not sockpath:
sys.stderr.write('No feedback socket specified\n')
sys.exit(1)
callbacksock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
callbacksock.connect(sockpath)
class ResultsCollector(CallbackBase):
def v2_runner_on_unreachable(self, result):
print_result(result, 'UNREACHABLE', self)
print_result(result, 'UNREACHABLE', self, callbacksock)
def v2_runner_on_ok(self, result, *args, **kwargs):
print_result(result, 'ok')
print_result(result, 'ok', self, callbacksock)
def v2_runner_on_failed(self, result, *args, **kwargs):
print_result(result, 'FAILED', self)
print_result(result, 'FAILED', self, callbacksock)
context.CLIARGS = ImmutableDict(
connection='smart', module_path=['/usr/share/ansible'], forks=10,
+29 -15
View File
@@ -395,7 +395,7 @@ def handle_request(env, start_response):
keymap = ckeymap
try:
tdc = util.run(['timedatectl'])[0].split(b'\n')
except subprocess.CalledProcessError:
except (subprocess.CalledProcessError, FileNotFoundError):
tdc = []
currtzvintage = time.time()
ncfg['timezone'] = currtz
@@ -519,19 +519,7 @@ def handle_request(env, start_response):
yield ''
elif env['PATH_INFO'].startswith('/self/remoteconfig/') and 'POST' == operation:
scriptcat = env['PATH_INFO'].replace('/self/remoteconfig/', '')
playlist = []
for privacy in ('public', 'private'):
slist, profile = get_scriptlist(
scriptcat, cfg, nodename,
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
profile, scriptcat, privacy)
if not os.path.isdir(dirname):
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
profile, scriptcat, privacy)
for filename in slist:
if filename.endswith('.yaml') or filename.endswith('.yml'):
playlist.append(os.path.join(dirname, filename))
playlist = list_ansible_scripts(cfg, nodename, scriptcat)
if playlist:
runansible.run_playbooks(playlist, [nodename])
start_response('202 Queued', ())
@@ -590,8 +578,13 @@ def handle_request(env, start_response):
yield 'No profile'
return
fname = '/var/lib/confluent/private/os/{}/{}'.format(profile, fname)
fullpath = os.path.abspath(fname)
if not fullpath.startswith('/var/lib/confluent/private/os/{}/'.format(profile)):
start_response('400 Bad Request', ())
yield 'Bad Request'
return
try:
with open(fname, 'rb') as privdata:
with open(fullpath, 'rb') as privdata:
start_response('200 OK', ())
yield privdata.read()
return
@@ -603,6 +596,22 @@ def handle_request(env, start_response):
start_response('404 Not Found', ())
yield 'Not found'
def list_ansible_scripts(cfg, nodename, scriptcat):
playlist = []
for privacy in ('public', 'private'):
slist, profile = get_scriptlist(
scriptcat, cfg, nodename,
'/var/lib/confluent/{0}/os/{{0}}/ansible/{{1}}'.format(privacy))
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}/'.format(
profile, scriptcat, privacy)
if not os.path.isdir(dirname):
dirname = '/var/lib/confluent/{2}/os/{0}/ansible/{1}.d/'.format(
profile, scriptcat, privacy)
for filename in slist:
if filename.endswith('.yaml') or filename.endswith('.yml'):
playlist.append(os.path.join(dirname, filename))
return playlist
def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
if '..' in scriptcat:
return None, None
@@ -619,6 +628,11 @@ def get_scriptlist(scriptcat, cfg, nodename, pathtemplate):
'deployment.profile', {}).get('value', '')
slist = []
target = pathtemplate.format(profile, scriptcat)
target = os.path.abspath(target)
allowedbase = os.path.abspath(pathtemplate.format(profile, '').rstrip('/'))
allowedbaseprefix = os.path.join(allowedbase, '')
if not target.startswith(allowedbaseprefix):
return None, None
if not os.path.isdir(target) and os.path.isdir(target + '.d'):
target = target + '.d'
try:
+66 -10
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
# Copyright 2016-2025 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -26,21 +26,59 @@ import eventlet
from eventlet.support.greendns import getaddrinfo
import pysnmp.smi.error as snmperr
import socket
import asyncio
snmp = eventlet.import_patched('pysnmp.hlapi')
asyn = False
if not hasattr(snmp, 'UsmUserData'):
# pysnmp that dropped the sync support
import pysnmp.hlapi.v3arch.asyncio as snmp
asyn = True
import pysnmp.smi.rfc1902 as rfc1902
def get_loop():
try:
return asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
def _run_coro(coro):
loop = get_loop()
fun = asyncio.wait_for(coro, None)
if loop.is_running():
task = asyncio.ensure_future(fun)
return loop.run_until_complete(task)
return loop.run_until_complete(fun)
async def _agen_to_list(agen):
out = []
async for item in agen:
out.append(item)
return out
def _sync_gen(agen):
return _run_coro(_agen_to_list(agen))
def _get_transport(name):
# Annoyingly, pysnmp does not automatically determine ipv6 v ipv4
res = getaddrinfo(name, 161, 0, socket.SOCK_DGRAM)
if res[0][0] == socket.AF_INET6:
return snmp.Udp6TransportTarget(res[0][4], 2)
if asyn:
return _run_coro(snmp.Udp6TransportTarget.create(res[0][4], 2))
else:
return snmp.Udp6TransportTarget(res[0][4], 2)
else:
return snmp.UdpTransportTarget(res[0][4], 2)
if asyn:
return _run_coro(snmp.UdpTransportTarget.create(res[0][4], 2))
else:
return snmp.UdpTransportTarget(res[0][4], 2)
class Session(object):
def __init__(self, server, secret, username=None, context=None):
def __init__(self, server, secret, username=None, context=None, privacy_protocol=None):
"""Create a new session to interrogate a switch
If username is not given, it is assumed that
@@ -59,9 +97,17 @@ class Session(object):
# SNMP v2c
self.authdata = snmp.CommunityData(secret, mpModel=1)
else:
if privacy_protocol == 'aes':
privproto = snmp.usmAesCfb128Protocol
elif privacy_protocol in ('des', None):
privproto = snmp.usmDESPrivProtocol
else:
raise exc.ConfluentException('Unsupported SNMPv3 privacy protocol '
'{0}'.format(privacy_protocol))
self.authdata = snmp.UsmUserData(
username, authKey=secret, privKey=secret,
authProtocol=snmp.usmHMACSHAAuthProtocol)
authProtocol=snmp.usmHMACSHAAuthProtocol,
privProtocol=privproto)
self.eng = snmp.SnmpEngine()
def walk(self, oid):
@@ -83,12 +129,22 @@ class Session(object):
if '::' in oid:
resolvemib = True
mib, field = oid.split('::')
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
if asyn:
obj = rfc1902.ObjectType(rfc1902.ObjectIdentity(mib, field))
else:
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
else:
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
lexicographicMode=False, lookupMib=resolvemib)
if asyn:
obj = rfc1902.ObjectType(rfc1902.ObjectIdentity(oid))
else:
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
if asyn:
walking = snmp.bulk_walk_cmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
lexicographicMode=False, lookupMib=resolvemib)
walking = _sync_gen(walking)
else:
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
lexicographicMode=False, lookupMib=resolvemib)
try:
for rsp in walking:
errstr, errnum, erridx, answers = rsp
+11 -6
View File
@@ -26,6 +26,7 @@ import ctypes.util
import errno
import os
import pwd
import shutil
import stat
import struct
import sys
@@ -458,7 +459,7 @@ def removesocket():
except OSError:
pass
def _unixdomainhandler():
def _unixdomainhandler(bind_group=None, bind_perms=None):
unixsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
os.remove("/var/run/confluent/api.sock")
@@ -466,10 +467,12 @@ def _unixdomainhandler():
pass
if not os.path.isdir("/var/run/confluent"):
os.makedirs('/var/run/confluent', 0o755)
oldumask = os.umask(0o777 - bind_perms)
unixsocket.bind("/var/run/confluent/api.sock")
os.chmod("/var/run/confluent/api.sock",
stat.S_IWOTH | stat.S_IROTH | stat.S_IWGRP |
stat.S_IRGRP | stat.S_IWUSR | stat.S_IRUSR)
os.chmod("/var/run/confluent/api.sock", bind_perms)
if bind_group:
shutil.chown("/var/run/confluent/api.sock", group=bind_group)
os.umask(oldumask)
atexit.register(removesocket)
unixsocket.listen(5)
while True:
@@ -498,11 +501,13 @@ def _unixdomainhandler():
class SockApi(object):
def __init__(self, bindhost=None, bindport=None):
def __init__(self, bindhost=None, bindport=None, bind_group=None, bind_perms=None):
self.tlsserver = None
self.unixdomainserver = None
self.bind_host = bindhost or '::'
self.bind_port = bindport or 13001
self.bind_group = bind_group
self.bind_perms = bind_perms or 0o666
def start(self):
global auditlog
@@ -515,7 +520,7 @@ class SockApi(object):
else:
eventlet.spawn_n(self.watch_for_cert)
eventlet.spawn_n(self.watch_resolv)
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
self.unixdomainserver = eventlet.spawn(_unixdomainhandler, self.bind_group, self.bind_perms)
def watch_resolv(self):
while True:
+119 -17
View File
@@ -19,7 +19,9 @@
import base64
import confluent.exceptions as cexc
import confluent.log as log
import glob
import hashlib
import ipaddress
try:
import psutil
except ImportError:
@@ -31,6 +33,12 @@ import socket
import ssl
import struct
import eventlet.green.subprocess as subprocess
import cryptography.x509 as x509
try:
import cryptography.x509.verification as verification
except ImportError:
verification = None
def mkdirp(path, mode=0o777):
@@ -86,6 +94,53 @@ def list_interface_indexes():
return
def get_bmc_subject_san(configmanager, nodename, addnames=()):
bmc_san = []
subject = ''
ipas = set([])
dnsnames = set([])
for addname in addnames:
try:
addr = ipaddress.ip_address(addname)
ipas.add(addname)
except Exception:
dnsnames.add(addname)
nodecfg = configmanager.get_node_attributes(nodename,
('dns.domain', 'hardwaremanagement.manager', 'hardwaremanagement.manager_tls_name'))
bmcaddr = nodecfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', '')
domain = nodecfg.get(nodename, {}).get('dns.domain', {}).get('value', '')
isipv4 = False
if bmcaddr:
bmcaddr = bmcaddr.split('/', 1)[0]
bmcaddr = bmcaddr.split('%', 1)[0]
dnsnames.add(bmcaddr)
subject = bmcaddr
if ':' in bmcaddr:
ipas.add(bmcaddr)
dnsnames.add('{0}.ipv6-literal.net'.format(bmcaddr.replace(':', '-')))
else:
try:
socket.inet_aton(bmcaddr)
isipv4 = True
ipas.add(bmcaddr)
except socket.error:
pass
if not isipv4: # neither ipv6 nor ipv4, should be a name
if domain and domain not in bmcaddr:
dnsnames.add('{0}.{1}'.format(bmcaddr, domain))
bmcname = nodecfg.get(nodename, {}).get('hardwaremanagement.manager_tls_name', {}).get('value', '')
if bmcname:
subject = bmcname
dnsnames.add(bmcname)
if domain and domain not in bmcname:
dnsnames.add('{0}.{1}'.format(bmcname, domain))
for dns in dnsnames:
bmc_san.append('DNS:{0}'.format(dns))
for ip in ipas:
bmc_san.append('IP:{0}'.format(ip))
return subject, ','.join(bmc_san)
def list_ips():
# Used for getting addresses to indicate the multicast address
# as well as getting all the broadcast addresses
@@ -184,23 +239,57 @@ def cert_matches(fingerprint, certificate):
return newfp and fingerprint == newfp
_polbuilder = None
class TLSCertVerifier(object):
def __init__(self, configmanager, node, fieldname):
def __init__(self, configmanager, node, fieldname, subject=None):
self.cfm = configmanager
self.node = node
self.fieldname = fieldname
self.subject = subject
def verify_by_ca(self, certificate):
global _polbuilder
_polbuilder = None
if not _polbuilder:
certs = []
for cert in glob.glob('/var/lib/confluent/public/site/tls/*.pem'):
with open(cert, 'rb') as certfile:
certs.extend(x509.load_pem_x509_certificates(certfile.read()))
if not certs:
return False
castore = verification.Store(certs)
_polbuilder = verification.PolicyBuilder()
eep = verification.ExtensionPolicy.permit_all().require_present(
x509.SubjectAlternativeName, verification.Criticality.AGNOSTIC, None).may_be_present(
x509.KeyUsage, verification.Criticality.AGNOSTIC, None)
cap = verification.ExtensionPolicy.webpki_defaults_ca().require_present(
x509.BasicConstraints, verification.Criticality.AGNOSTIC, None).may_be_present(
x509.KeyUsage, verification.Criticality.AGNOSTIC, None)
_polbuilder = _polbuilder.store(castore).extension_policies(
ee_policy=eep, ca_policy=cap)
try:
addr = ipaddress.ip_address(self.subject)
subject = x509.IPAddress(addr)
except ValueError:
subject = x509.DNSName(self.subject)
cert = x509.load_der_x509_certificate(certificate)
_polbuilder.build_server_verifier(subject).verify(cert, [])
return True
def verify_cert(self, certificate):
storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
)
if (self.fieldname not in storedprint[self.node] or
storedprint[self.node][self.fieldname]['value'] == ''):
storedprint = storedprint.get(self.node, {}).get(self.fieldname, {}).get('value', '')
newpolicy = self.cfm.get_node_attributes(self.node,
('pubkeys.addpolicy',))
newpolicy = newpolicy.get(self.node, {}).get('pubkeys.addpolicy', {}).get('value', '')
if not storedprint:
# no stored value, check policy for next action
newpolicy = self.cfm.get_node_attributes(self.node,
('pubkeys.addpolicy',))
if ('pubkeys.addpolicy' in newpolicy[self.node] and
'value' in newpolicy[self.node]['pubkeys.addpolicy'] and
newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
if newpolicy == 'manual':
# manual policy means always raise unless a match is set
# manually
fingerprint = get_fingerprint(certificate, 'sha256')
@@ -209,17 +298,30 @@ class TLSCertVerifier(object):
self.fieldname, 'newkey')
# since the policy is not manual, go ahead and add new key
# after logging to audit log
fingerprint = get_fingerprint(certificate, 'sha256')
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'certautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {self.fieldname: fingerprint}})
return True
elif cert_matches(storedprint[self.node][self.fieldname]['value'],
certificate):
if newpolicy in ('tofu', ''):
fingerprint = get_fingerprint(certificate, 'sha256')
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'certautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {self.fieldname: fingerprint}})
return True
elif cert_matches(storedprint, certificate) and newpolicy != 'ca-only':
return True
fingerprint = get_fingerprint(certificate, 'sha256')
# No pinned certificate match, try to validate by CA if possible
if self.subject:
try:
if verification and self.verify_by_ca(certificate):
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'certautoupdate',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {self.fieldname: fingerprint}})
return True
except Exception:
if newpolicy == 'ca-only':
raise
raise cexc.PubkeyInvalid(
'Mismatched certificate detected', certificate, fingerprint,
self.fieldname, 'mismatch')
+1 -1
View File
@@ -25,7 +25,7 @@ Requires: python-pyghmi >= 1.5.71, python-eventlet, python-greenlet, python-pycr
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-enum34, python3-asn1crypto, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
%else
%if "%{dist}" == ".el9"
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-netifaces, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-netifaces, python3-pyasn1 >= 0.2.3, ((python3-pysnmp >= 4.3.4 and python3-pysnmp < 5.0.0) or python3-pysnmp >= 7.1.21), python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
%else
%if "%{dist}" == ".el10"
Requires: python3-pyghmi >= 1.5.71, python3-eventlet, python3-greenlet, python3-pycryptodomex >= 3.4.7, confluent_client == %{version}, python3-pyparsing, python3-paramiko, python3-dns, python3-webauthn, python3-psutil, python3-pyasn1 >= 0.2.3, python3-pysnmp >= 4.3.4, python3-lxml, python3-eficompressor, python3-setuptools, python3-dateutil, python3-cffi, python3-pyOpenSSL, python3-websocket-client python3-msgpack python3-libarchive-c python3-yaml openssl iproute
+7
View File
@@ -1,3 +1,4 @@
cd $(dirname $0)
VERSION=`git describe|cut -d- -f 1`
NUMCOMMITS=`git describe|cut -d- -f 2`
if [ "$NUMCOMMITS" != "$VERSION" ]; then
@@ -12,6 +13,7 @@ cp ../LICENSE NOTICE *.c *.h Makefile dist/confluent_vtbufferd-$VERSION
cd dist
tar czf confluent_vtbufferd-$VERSION.tar.gz confluent_vtbufferd-$VERSION
cd -
mkdir -p ~/rpmbuild/SOURCES ~/rpmbuild/SPECS
cp dist/confluent_vtbufferd-$VERSION.tar.gz ~/rpmbuild/SOURCES
sed -e 's/#VERSION#/'$VERSION/ confluent_vtbufferd.spec.tmpl > ~/rpmbuild/SPECS/confluent_vtbufferd.spec
rpmbuild -ba ~/rpmbuild/SPECS/confluent_vtbufferd.spec 2> /dev/null |grep ^Wrote:
@@ -22,5 +24,10 @@ else
# Clean up the generated files in this directory
rm -rf dist
fi
ARCH=$(uname -m)
if [ ! -z "$1" ]; then
cp /root/rpmbuild/RPMS/$ARCH/confluent_vtbufferd-*.$ARCH.rpm $1
fi
+10 -3
View File
@@ -1,7 +1,14 @@
FROM almalinux:8
FROM almalinux:10
VOLUME /var/lib/confluent
VOLUME /var/run/confluent
VOLUME /etc/confluent
VOLUME /var/lib/tftpboot
ENV EMBEDHTTP=1
ENV EMBEDTFTP=0
RUN ["yum", "-y", "update"]
RUN ["rpm", "-ivh", "https://hpc.lenovo.com/yum/latest/el8/x86_64/lenovo-hpc-yum-1-1.x86_64.rpm"]
RUN ["yum", "-y", "install", "lenovo-confluent", "tftp-server", "openssh-clients", "openssl", "vim-enhanced", "iproute"]
RUN ["rpm", "-ivh", "https://hpc.lenovo.com/yum/latest/el10/x86_64/lenovo-hpc-yum-1-1.x86_64.rpm"]
RUN ["yum", "-y", "install", "lenovo-confluent", "tftp-server", "openssh-clients", "openssl", "vim-enhanced", "iproute", "policycoreutils", "selinux-policy-targeted", "device-mapper"]
RUN ["chown", "-r", "root", "/etc/confluent"]
ADD runconfluent.sh /bin/
CMD ["/bin/bash", "/bin/runconfluent.sh"]
+5 -1
View File
@@ -19,4 +19,8 @@ fuse-libs
libnl3
dhcpcd
openssh-keysign
chrony kernel net-tools nfs-utils openssh-server rsync tar util-linux python3 tar dracut dracut-network ethtool parted openssl openssh-clients bash vim-minimal rpm iputils lvm2 efibootmgr shim-x64.x86_64 grub2-efi-x64 attr
chrony kernel net-tools nfs-utils openssh-server rsync tar util-linux python3 tar dracut dracut-network ethtool parted openssl openssh-clients bash vim-minimal rpm iputils lvm2 efibootmgr attr
%onlyarch x86_64
shim-x64.x86_64 grub2-efi-x64
%onlyarch aarch64
shim-aa64.aarch64 grub2-efi-aa64
+6 -1
View File
@@ -1,3 +1,4 @@
system-release
dnf
hostname
irqbalance
@@ -17,4 +18,8 @@ xfsprogs
e2fsprogs
fuse-libs
libnl3
chrony kernel net-tools nfs-utils openssh-server rsync tar util-linux python3 tar dracut dracut-network ethtool parted openssl dhclient openssh-clients bash vim-minimal rpm iputils lvm2 efibootmgr shim-x64.x86_64 grub2-efi-x64 attr
chrony kernel net-tools nfs-utils openssh-server rsync tar util-linux python3 tar dracut dracut-network ethtool parted openssl dhclient openssh-clients bash vim-minimal rpm iputils lvm2 efibootmgr attr
%onlyarch x86_64
shim-x64.x86_64 grub2-efi-x64
%onlyarch aarch64
shim-aa64.aarch64 grub2-efi-aa64
+25 -6
View File
@@ -214,7 +214,7 @@ def capture_remote(args):
subprocess.check_call(['rsync', __file__, '{0}:/run/imgutil/capenv/'.format(targ)])
finfo = subprocess.check_output(['ssh', targ, 'python3', '/run/imgutil/capenv/imgutil', 'getfingerprint']).decode('utf8')
finfo = json.loads(finfo)
if finfo['oscategory'] not in ('el8', 'el9', 'ubuntu20.04', 'ubuntu22.04'):
if finfo['oscategory'] not in ('el8', 'el9', 'ubuntu20.04', 'ubuntu22.04', 'ubuntu24.04', 'ubuntu26.04'):
sys.stderr.write('Not yet supported for capture: ' + repr(finfo) + '\n')
sys.exit(1)
unmet = finfo.get('unmetprereqs', [])
@@ -386,6 +386,7 @@ def encrypt_image(plainfile, cryptfile, keyfile):
loopdev = subprocess.check_output(['losetup', '-f']).decode('utf8').strip()
subprocess.check_call(['losetup', loopdev, cryptfile])
subprocess.check_call(['dmsetup', 'create', dmname, '--table', '0 {} crypt aes-xts-plain64 {} 0 {} 8'.format(neededblocks, key, loopdev)])
subprocess.check_call(['dmsetup', 'mknodes', dmname])
with open('/dev/mapper/{}'.format(dmname), 'wb') as cryptout:
with open(plainfile, 'rb+') as plainin:
lastoffset = 0
@@ -519,9 +520,18 @@ class OsHandler(object):
pkglistfile = pkglistfile[:-1]
with open(pkglistfile, 'r') as pkglist:
pkgs = ''
ignoremode = False
for line in pkglist.readlines():
line = line.split('#', 1)[0].strip()
pkgs += line + ' '
if line.startswith(r'%onlyarch '):
archs = line[len(r'%onlyarch '):].split()
if self.arch not in archs:
ignoremode = True
else:
ignoremode = False
continue
if not ignoremode:
pkgs += line + ' '
pkgs = pkgs.split()
retpkgs = []
for pkg in pkgs:
@@ -764,7 +774,12 @@ class ElHandler(OsHandler):
if line.startswith('selinuxfs '):
break
else:
self.relabel_targdir()
try:
self.relabel_targdir()
except subprocess.CalledProcessError:
# Some filesystem contexts can not accommodate the selinux labels, warn that
# this failed, but allow it to proceed in case it can boot anyway
sys.stderr.write('Warning: could not relabel target filesystem for SELinux\n')
def relabel_targdir(self):
subprocess.check_call(
@@ -810,11 +825,14 @@ def version_sort(iterable):
def get_kern_version(filename):
with open(filename, 'rb') as kernfile:
checkgzip = kernfile.read(2)
if checkgzip == b'\x1f\x8b':
# gzipped... this would probably be aarch64
header = kernfile.read(2)
if header == b'\x1f\x8b':
# gzipped... we can't process this right now,
# assume the filename has the version embedded
return os.path.basename(filename).replace('vmlinuz-', '')
if header == b'MZ':
# PE format, no easy way to get version, assume filename has it
return os.path.basename(filename).replace('vmlinuz-', '')
kernfile.seek(0x20e)
offset = struct.unpack('<H', kernfile.read(2))[0] + 0x200
kernfile.seek(offset)
@@ -1278,6 +1296,7 @@ def prep_decrypt(indir):
tempfile.mktemp()
subprocess.check_call(['dmsetup', 'create', dmname, '--table', '0 {0} crypt {1} {2} 0 {3} 8'.format(
imglen, cipher, key, loopdev)])
subprocess.check_call(['dmsetup', 'mknodes', dmname])
return '/dev/mapper/{0}'.format(dmname), loopdev
@@ -34,10 +34,12 @@ copy_exec /usr/bin/ssh-keygen
copy_exec /usr/sbin/sshd
copy_exec /usr/sbin/mkfs.xfs
copy_exec /usr/lib/x86_64-linux-gnu/libfuse.so.2
copy_exec /usr/lib/aarch64-linux-gnu/libfuse.so.2
copy_exec /usr/bin/dirname
[ -e $DESTDIR/usr/sbin/losetup ] && rm $DESTDIR/usr/sbin/losetup
copy_exec /usr/sbin/losetup
copy_exec /usr/lib/x86_64-linux-gnu/libtss2-tcti-device.so.0
copy_exec /usr/lib/aarch64-linux-gnu/libtss2-tcti-device.so.0
manual_add_modules e1000 e1000e igb sfc mlx5_ib mlx5_core mlx4_en cxgb3 cxgb4
manual_add_modules tg3 bnx2 bnx2x bna ixgb ixgbe qlge mptsas mpt2sas mpt3sas
manual_add_modules megaraid_sas ahci xhci-hcd sd_mod pmcraid be2net vfat ext3
+11 -2
View File
@@ -9,7 +9,13 @@ if [ -z "$TARGPROF" ]; then
echo "Target profile must be specified"
exit 1
fi
nodedefine $TARGNODE deployment.apiarmed=once deployment.profile=$TARGPROF deployment.useinsecureprotocols= deployment.pendingprofile=$TARGPROF
OLDINSECURE=$(nodeattrib $TARGNODE deployment.useinsecureprotocols -b 2> /dev/null |grep -v inherited|awk '{print $3}')
nodedefine $TARGNODE deployment.profile=$TARGPROF deployment.useinsecureprotocols= deployment.pendingprofile=$TARGPROF
confetty set /nodes/$TARGNODE/deployment/ident_image=create
REMTMP=$(ssh $TARGNODE mktemp -d)
scp /var/lib/confluent/private/identity_files/$TARGNODE.json $TARGNODE:$REMTMP
rm /var/lib/confluent/private/identity_files/$TARGNODE.*
rm /var/lib/confluent/private/identity_images/$TARGNODE.*
cat /var/lib/confluent/public/site/ssh/*pubkey | ssh $TARGNODE "mkdir -p /root/.ssh/; cat - >> /root/.ssh/authorized_keys"
ssh $TARGNODE mkdir -p /etc/confluent /opt/confluent/bin
cat /var/lib/confluent/public/site/tls/*.pem | ssh $TARGNODE "cat - >> /etc/confluent/ca.pem"
@@ -17,8 +23,11 @@ cat /var/lib/confluent/public/site/tls/*.pem | ssh $TARGNODE "cat - >> /etc/pki/
nodeattrib $TARGNODE id.uuid=$(ssh $TARGNODE cat /sys/devices/virtual/dmi/id/product_uuid)
scp prepadopt.sh $TARGNODE:/tmp/
scp finalizeadopt.sh $TARGNODE:/tmp/
ssh $TARGNODE bash /tmp/prepadopt.sh $TARGNODE $TARGPROF
ssh $TARGNODE bash /tmp/prepadopt.sh $TARGNODE $TARGPROF $REMTMP/$TARGNODE.json
nodeattrib $TARGNODE deployment.pendingprofile=
nodeattrib $TARGNODE -c deployment.useinsecureprotocols
if [ ! -z "$OLDINSECURE" ]; then
nodeattrib $TARGNODE $OLDINSECURE
fi
nodeapply $TARGNODE -k
ssh $TARGNODE sh /tmp/finalizeadopt.sh
+5 -2
View File
@@ -1,6 +1,7 @@
#!/bin/bash
TARGNODE=$1
TARGPROF=$2
TARGIDENT=$3
TMPDIR=$(mktemp -d)
cd $TMPDIR
DEPLOYSRV=$(echo $SSH_CLIENT|awk '{print $1}')
@@ -16,10 +17,12 @@ cpio -dumi < addons.cpio
systemctl status firewalld >& /dev/null && FWACTIVE=1
if [ "$FWACTIVE" == 1 ]; then systemctl stop firewalld; fi
opt/confluent/bin/copernicus > /etc/confluent/confluent.info
opt/confluent/bin/clortho $TARGNODE $DEPLOYSRV > /etc/confluent/confluent.apikey
#opt/confluent/bin/clortho $TARGNODE $DEPLOYSRV > /etc/confluent/confluent.apikey
. /etc/confluent/functions
confluentpython opt/confluent/bin/apiclient -i $TARGIDENT /confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
if [ "$FWACTIVE" == 1 ]; then systemctl start firewalld; fi
cp opt/confluent/bin/apiclient /opt/confluent/bin
curl -sg -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -H "CONFLUENT_NODENAME: $TARGNODE" https://$UDEPLOYSRV/confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
#curl -sg -H "CONFLUENT_APIKEY: $(cat /etc/confluent/confluent.apikey)" -H "CONFLUENT_NODENAME: $TARGNODE" https://$UDEPLOYSRV/confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
# python3 /opt/confluent/bin/apiclient /confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
cd -
echo rm -rf $TMPDIR
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
if [[ -z "$SWITCHUSER" || -z "$SWITCHPASS" ]]; then
echo "Error: SWITCHUSER and SWITCHPASS environment variables must be set"
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "Error: At least one argument is required"
exit 1
fi
SWITCHNAME="$1"
# Create temporary SSH ASKPASS script
ASKPASS_SCRIPT=$(mktemp)
trap "rm -f $ASKPASS_SCRIPT" EXIT
cat > "$ASKPASS_SCRIPT" << 'EOF'
#!/bin/bash
echo "$SWITCHPASS"
EOF
chmod 700 "$ASKPASS_SCRIPT"
# Set SSH_ASKPASS environment variable
export SSH_ASKPASS="$ASKPASS_SCRIPT"
export SSH_ASKPASS_REQUIRE=force
export DISPLAY=none
CERTDIR=$(mktemp -d)
trap "rm -rf $CERTDIR" EXIT
cd "$CERTDIR"
python3 /opt/confluent/lib/python/confluent/certutil.py -s "$SWITCHNAME"
setsid ssh $SWITCHUSER@"$SWITCHNAME" <<EOC
enter candidate exclusive
/ system json-rpc-server admin-state enable
/ system tls server-profile fullcert key "$(<$CERTDIR/key.pem)"
/ system tls server-profile fullcert certificate "$(<$CERTDIR/cert.pem)"
/ system json-rpc-server network-instance mgmt https admin-state enable
/ system json-rpc-server network-instance mgmt https tls-profile fullcert
commit save
EOC