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

Compare commits

..

164 Commits

Author SHA1 Message Date
Jarrod Johnson a9ba385996 Fix type of maxnodes 2026-08-21 14:58:13 -04:00
Jarrod Johnson 29a9d417d6 Do not aggressively respawn buffer daemon.
Start buffer daemon only when needed.

Limit restarts to once every 30 seconds.
2026-08-20 07:23:40 -04:00
Jarrod Johnson 7347125b6f Disable implicit tenant creation
If we support more tenants, we will modify that branch.
2026-07-16 12:39:51 -04:00
Jarrod Johnson 66347a937e Ensure prefix is a string 2026-07-14 11:01:32 -04:00
Jarrod Johnson ee01dfd05e Add UEFI HTTP boot for arm64 to recognized archs
For now, we serve up the whole image, no need to distinguish arm from x86 here yet
2026-07-14 09:52:27 -04:00
Jarrod Johnson 611e1f5bc3 Fix nodemedia attach
The hardening blocked all URL patters.
2026-07-14 09:48:09 -04:00
Jarrod Johnson 7622145fea Fall back to x64 if the aarch64 location didn't pan out 2026-07-13 10:01:04 -04:00
Jarrod Johnson 678bd53857 Correct paths to aarch64 boot material in imgutil 2026-07-13 09:23:33 -04:00
Jarrod Johnson 3295778566 Improve webauthn error handling and tighten up routing
If not webauthn, have authorize
bail out on webauthn request instead
of a sessionless authdata.

For some "special" HTTP paths, tighten up routing rules.
2026-07-08 12:03:48 -04:00
Jarrod Johnson b3acf011bd Restart vtbufferd on exit
Notably, if you strace it, it will trigger an exit(1).  There was at least one documented segmentation fault as well.

Buffer content is lost in such an event, but service remains running.
2026-07-02 12:20:01 -04:00
Jarrod Johnson dc4cafc29f Rework autoconsole logic
Match autocons

Skip unless EFI x86_64.

If SPCR, trust it and use that unconditionally.

Otherwise, if only one can respond to TIOCMGET, then use that one.

If multiple can respond, but exactly one shows carrier, use that.
2026-06-25 16:41:32 -04:00
Jarrod Johnson 3340585fb4 Only count copernicus replies that have OK status 2026-06-25 15:24:31 -04:00
Jarrod Johnson 4456767122 When possible, check confluent user access to file
If a confluent user is a system user, do not allow them to
upload paths that their user would not have access to otherwise.

For non-system users, continue with the path based banned behavior.
2026-06-25 12:15:13 -04:00
Jarrod Johnson 556bb1d0ff Prevent staging of files from indicating path traversal 2026-06-25 10:09:59 -04:00
Jarrod Johnson a201e9886e Have messages force normalizing the incoming filenames
This avoids downstream code that may expect specific locations from being confused.
2026-06-25 09:48:40 -04:00
Jarrod Johnson f82993efe7 Fix debian deployment on slow network link up
When network link was slow to establish, it would fall right through
the network initilalization code.

Now keep working it until a result is acheived.
2026-06-25 08:37:19 -04:00
Jarrod Johnson f8366a50ef Do not set 0.0.0.0 gateway 2026-06-24 15:59:10 -04:00
Jarrod Johnson d369dcac55 Fix non-bonding configuration of ubuntu 2026-06-16 12:42:03 -04:00
Jarrod Johnson 3b2d92a219 Correct typo in pthread name 2026-06-08 11:00:40 -04:00
Jarrod Johnson ffacb66c62 Update rdma vintage 2026-06-08 10:56:26 -04:00
Jarrod Johnson 2073f421da Add libraries to genesis 2026-06-08 10:54:50 -04:00
Jarrod Johnson ed2eed66dc Allow nodedeploy to request http boot specifically 2026-06-03 09:28:44 -04:00
Jarrod Johnson d8f9b6c8e6 Add support for http boot
Some redfish require us to be very specific.
2026-06-03 09:12:46 -04:00
Jarrod Johnson d9a18a7bf6 Actually use the interposer for firmware update 2026-06-01 19:52:43 -04:00
Markus Hilger 439a930188 confignet: Fix interface type detection for IB VFs
IB VFs have the following "ip l" output:

4: ibp129s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 2044 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/infiniband 00:00:00:8d:fe:80:00:00:00:00:00:00:60:5e:65:03:00:2c:43:c8 brd 00:ff:ff:ff:ff:12:40:1b:ff:ff:00:00:00:00:00:00:ff:ff:ff:ff
    vf 0     link/infiniband 00:00:00:8d:fe:80:00:00:00:00:00:00:60:5e:65:03:00:2c:43:c8 brd 00:ff:ff:ff:ff:12:40:1b:ff:ff:00:00:00:00:00:00:ff:ff:ff:ff, spoof checking off, NODE_GUID 00:00:00:00:00:00:00:00, PORT_GUID 00:00:00:00:00:00:00:00, link-state enable, trust off, query_rss off
5: eno1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    link/ether 30:56:0f:17:c0:b4 brd ff:ff:ff:ff:ff:ff
    altname enp196s0
    altname enx30560f17c0b4

This breaks the detection script because index 0 of the "vf 0 ..." line is not link/<type> anymore.
This commit improves the detection logic to fix this.
2026-06-01 19:30:54 -04:00
Jarrod Johnson 00b2afd42b Fixes for confignet for Ubuntu
Try to find various layers of network config and normalize.

Ultimately, after post subiquity will do some things and easiest to fix in firstboot instead.
2026-06-01 16:49:42 -04:00
Jarrod Johnson 90c2a4cf73 Fix iterating the netplan configuration 2026-06-01 12:55:11 -04:00
Jarrod Johnson c691dc7159 Remove cloud-init netplan if redundant 2026-06-01 09:33:25 -04:00
Jarrod Johnson a7c188b812 Add support for passing a parameterfile in updates 2026-06-01 07:51:22 -04:00
Jarrod Johnson 5ba43ecaa0 Add bonding to netplan management 2026-05-26 10:06:49 -04:00
Vinícius Ferrão 2f53d3bde6 Include xen-front drivers in confluent-curated initramfs 2026-05-23 14:21:13 -04:00
Jarrod Johnson 9fe9e8672a Support more states 2026-05-21 10:03:08 -04:00
Jarrod Johnson 0fe60175f3 Add missing close 2026-05-21 08:36:16 -04:00
Jarrod Johnson d411041243 Recognize more storage states 2026-05-20 16:14:24 -04:00
Jarrod Johnson cc101d12bc Port diskless enhancements from el9 to ubuntu 2026-05-20 15:30:23 -04:00
Jarrod Johnson 2f08ee81f2 Fix off by one in urlmount 2026-05-20 12:37:36 -04:00
Jarrod Johnson 1e9231eea6 Add megasol method 2026-05-19 09:17:50 -04:00
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
116 changed files with 3412 additions and 547 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)
+2 -1
View File
@@ -78,6 +78,7 @@ def main(args):
ap = argparse.ArgumentParser(description='Deploy OS to nodes')
ap.add_argument('-c', '--clear', help='Clear any pending deployment action', action='store_true')
ap.add_argument('-n', '--network', help='Initiate deployment over PXE/HTTP', action='store_true')
ap.add_argument('-b', '--bootmethod', help='Specify network boot method (e.g., network, http)', default='network')
ap.add_argument('-p', '--prepareonly', help='Prepare only, skip any interaction with a BMC associated with this deployment action', action='store_true')
ap.add_argument('-m', '--maxnodes', help='Specifiy a maximum nodes to be deployed')
ap.add_argument('-r', '--redeploy', help='Redeploy nodes with the current or pending profile', action='store_true')
@@ -213,7 +214,7 @@ def main(args):
print('{0}: {1}{2}'.format(node, profile, armed))
sys.exit(0)
if not args.clear and args.network and not args.prepareonly:
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', 'network',
rc = c.simple_noderange_command(args.noderange, '/boot/nextdevice', args.bootmethod,
bootmode='uefi',
persistent=False,
errnodes=errnodes)
+6
View File
@@ -59,6 +59,8 @@ argparser = optparse.OptionParser(
"%prog <noderange> [list][updatestatus][update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
argparser.add_option('-p', '--parameterfile', type='string',
help='When updating, use the specified parameter file')
argparser.add_option('-m', '--maxnodes', type='int',
help='When updating, prompt if more than the specified '
'number of servers will be affected')
@@ -112,6 +114,10 @@ def update_firmware(session, filename):
upargs = {'filename': filename}
if options.backup:
upargs['bank'] = 'backup'
if options.parameterfile:
with open(options.parameterfile, 'rb') as pf:
pfdata = pf.read()
upargs['parameterdata'] = pfdata
noderrs = {}
if session.unixdomain:
filesbynode = {}
+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']]
+1 -1
View File
@@ -32,7 +32,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='Usage: %prog [options] <noderange> [default|cd|network|setup|hd|usb|floppy]')
usage='Usage: %prog [options] <noderange> [default|cd|network|http|setup|hd|usb|floppy]')
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
+8 -2
View File
@@ -281,6 +281,7 @@ class Command(object):
if maxnodes is None:
return
nsize = self.get_noderange_size(noderange)
maxnodes = int(maxnodes)
if nsize > maxnodes:
if nsize == 1:
nodename = list(self.read(
@@ -391,8 +392,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
+4 -1
View File
@@ -35,7 +35,10 @@ the out of band facilities. Firmware updates can end in one of three states:
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
When updating, prompt if more than the specified number of servers will
be affected
* `-p PARAMETERFILE`, `--paramaterfile=PARAMETERFILE`:
For updating, a parameter file to provife along with the update payload
* `-h`, `--help`:
Show help message and exit
@@ -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))
@@ -10,11 +10,13 @@
# serial port is not reporting DCD, then it doesn't look like a comfortable enough scenario
import fcntl
import glob
import os
import os.path
import struct
import subprocess
import termios
import platform
addrtoname = {
@@ -74,9 +76,8 @@ def fixup_ubuntu_grub_serial():
grubout.write(grubline + '\n')
subprocess.check_call(['update-grub'])
def get_serial_config():
if not os.path.exists('/sys/firmware/efi'):
return None
def get_spcr_config():
if not os.path.exists('/sys/firmware/acpi/tables/SPCR'):
return None
spcr = open("/sys/firmware/acpi/tables/SPCR", "rb")
@@ -99,14 +100,39 @@ def get_serial_config():
currattr = termios.tcgetattr(ttyf)
currattr[4:6] = [0, termiobaud[retval['speed']]]
termios.tcsetattr(ttyf, termios.TCSANOW, currattr)
retval['connected'] = bool(struct.unpack('<I', fcntl.ioctl(
ttyf, termios.TIOCMGET, '\x00\x00\x00\x00'))[0] & termios.TIOCM_CAR)
os.close(ttyf)
return retval
def get_serial_config():
if platform.machine() != 'x86_64':
return None # Trust non-x86 to do the right thing
if not os.path.exists('/sys/firmware/efi'):
return None # BIOS might fail at grub output, defer to stock OS behavior
retval = get_spcr_config()
if retval:
return retval
firstfound = None
numpossible = 0
numconnected = 0
for serdev in glob.glob('/dev/ttyS*'):
ttyf = os.open(serdev, os.O_RDWR | os.O_NOCTTY)
try:
statusreg = fcntl.ioctl(ttyf, termios.TIOCMGET, '\x00\x00\x00\x00')
numpossible += 1
if not firstfound:
firstfound = serdev
except Exception:
continue
finally:
os.close(ttyf)
if struct.unpack('<I', statusreg)[0] & termios.TIOCM_CAR:
numconnected += 1
firstfound = serdev
if numpossible == 1 or numconnected == 1:
return { 'tty': firstfound, 'speed': 115200 }
def main():
autoconscfg = get_serial_config()
if not autoconscfg or not autoconscfg['connected']:
if not autoconscfg:
return
if os.path.exists('/etc/redhat-release'): # redhat family
deserialize_grub_rh()
@@ -80,14 +80,18 @@ def await_tentative():
time.sleep(1)
def map_idx_to_name():
map = {}
map_dict = {}
devtype = {}
prevdev = None
for line in subprocess.check_output(['ip', 'l']).decode('utf8').splitlines():
if line.startswith(' ') and 'link/' in line:
typ = line.split()[0].split('/')[1]
devtype[prevdev] = typ if typ != 'ether' else 'ethernet'
if line.startswith(' '):
if 'link/' in line and prevdev and prevdev not in devtype:
for word in line.split():
if word.startswith('link/'):
typ = word.split('/')[1]
devtype[prevdev] = typ if typ != 'ether' else 'ethernet'
break # Stop detection after the first type hit
continue
idx, iface, rst = line.split(':', 2)
prevdev = iface.strip()
@@ -99,9 +103,8 @@ def map_idx_to_name():
pass
idx = int(idx)
iface = iface.strip()
map[idx] = iface
return map, devtype
map_dict[idx] = iface
return map_dict, devtype
def get_interface_name(iname, settings):
explicitname = settings.get('interface_names', None)
@@ -114,8 +117,10 @@ def get_interface_name(iname, settings):
class NetplanManager(object):
def __init__(self, deploycfg):
self.cfgbydev = {}
self.cfgbybond = {}
self.read_connections()
self.deploycfg = deploycfg
self.teamidx = 0
def read_connections(self):
for plan in glob.glob('/etc/netplan/*.y*ml'):
@@ -124,29 +129,61 @@ class NetplanManager(object):
if not planinfo:
continue
nicinfo = planinfo.get('network', {}).get('ethernets', {})
for devname in nicinfo:
if devname == 'lo':
continue
if 'gateway4' in nicinfo[devname]:
# normalize deprecated syntax on read in
gw4 = nicinfo[devname]['gateway4']
del nicinfo[devname]['gateway4']
routeinfo = nicinfo[devname].get('routes', [])
for ri in routeinfo:
if ri.get('via', None) == gw4 and ri.get('to', None) in ('default', '0.0.0.0/0', '0/0'):
break
else:
routeinfo.append({
'to': 'default',
'via': gw4
})
nicinfo[devname]['routes'] = routeinfo
self.cfgbydev[devname] = nicinfo[devname]
bondinfo = planinfo.get('network', {}).get('bonds', {})
for currinfo in (nicinfo, bondinfo):
currcfg = self.cfgbydev if currinfo is nicinfo else self.cfgbybond
for devname in currinfo:
if devname == 'lo':
continue
if 'gateway4' in currinfo[devname]:
# normalize deprecated syntax on read in
gw4 = currinfo[devname]['gateway4']
del currinfo[devname]['gateway4']
routeinfo = currinfo[devname].get('routes', [])
for ri in routeinfo:
if ri.get('via', None) == gw4 and ri.get('to', None) in ('default', '0.0.0.0/0', '0/0'):
break
else:
routeinfo.append({
'to': 'default',
'via': gw4
})
currinfo[devname]['routes'] = routeinfo
currcfg[devname] = currinfo[devname]
def apply_configuration(self, cfg):
devnames = cfg['interfaces']
if len(devnames) != 1:
raise Exception('Multi-nic team/bonds not yet supported')
if len(devnames) > 1:
teammode = cfg['settings'].get('team_mode', None)
if not teammode:
sys.stderr.write("Warning, multiple interfaces ({0}) without a team_mode, skipping setup\n".format(','.join(devnames)))
return
if teammode == 'lacp':
teammode = '802.3ad'
elif teammode == 'activebackup':
teammode = 'active-backup'
for currdev in self.cfgbybond:
for iface in self.cfgbybond[currdev].get('interfaces', []):
if iface in devnames:
break
else:
continue
else:
continue
# this bond is identified as matching
self.cfgbybond[currdev]['interfaces'] = list(devnames)
self.cfgbybond[currdev]['parameters']['mode'] = teammode
devnames = [currdev]
break
# no current bond, make a new one
connname = cfg['settings'].get('connection_name', None)
if not connname:
connname = 'bond{0}'.format(self.teamidx)
while connname in self.cfgbybond:
self.teamidx += 1
connname = 'bond{0}'.format(self.teamidx)
self.cfgbybond[connname] = {'interfaces': list(devnames), 'parameters': {'mode': teammode, 'mii-monitor-interval': 100}}
devnames = [connname]
stgs = cfg['settings']
needcfgapply = False
for devname in devnames:
@@ -171,7 +208,7 @@ class NetplanManager(object):
gws.append(stgs.get('ipv4_gateway', None))
gws.append(stgs.get('ipv6_gateway', None))
for gwaddr in gws:
if gwaddr:
if gwaddr and gwaddr != '0.0.0.0':
cfgroutes = self.getcfgarrpath([devname, 'routes'])
for rinfo in cfgroutes:
if rinfo.get('via', None) == gwaddr:
@@ -192,18 +229,56 @@ class NetplanManager(object):
if dnsdomain not in currdnsdomain:
needcfgwrite = True
currdnsdomain.append(dnsdomain)
prune_from_cloudinit = []
if needcfgwrite:
needcfgapply = True
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
oumask = os.umask(0o77)
with open('/etc/netplan/{0}-confluentcfg.yaml'.format(devname), 'w') as planout:
if devname in self.cfgbydev:
prune_from_cloudinit.append(devname)
newcfg = {'network': {'version': 2, 'ethernets': {devname: self.cfgbydev[devname]}}}
cfgfile = '/etc/netplan/10-{0}-confluentcfg.yaml'.format(devname)
elif devname in self.cfgbybond:
newcfg = {'network': {'version': 2, 'bonds': {devname: self.cfgbybond[devname]}}}
for iface in newcfg['network']['bonds'][devname]['interfaces']:
prune_from_cloudinit.append(iface)
with open('/etc/netplan/10-{0}-confluentcfg.yaml'.format(iface), 'w') as planout:
planout.write(yaml.dump({'network': {'version': 2, 'ethernets': {iface: {'dhcp4': False}}}}))
cfgfile = '/etc/netplan/30-{0}-confluentcfg.yaml'.format(devname)
with open(cfgfile, 'w') as planout:
planout.write(yaml.dump(newcfg))
os.umask(oumask)
if prune_from_cloudinit:
prunecfgs = ['/etc/netplan/00-installer-config.yaml',
'/etc/netplan/50-cloud-init.yaml.dist-subiquity',
'/etc/netplan/50-cloud-init.yaml']
prunecfgs.extend(glob.glob('/etc/cloud/cloud.cfg.d/*.cfg'))
for defcfg in prunecfgs:
if not os.path.exists(defcfg):
continue
with open(defcfg, 'r') as cloudinit:
cloudinfo = yaml.safe_load(cloudinit)
if 'network' not in cloudinfo:
continue
for clouddev in list(cloudinfo.get('network', {}).get('ethernets', {})):
if clouddev in prune_from_cloudinit:
del cloudinfo['network']['ethernets'][clouddev]
if not cloudinfo['network'].get('ethernets', {}):
os.remove(defcfg)
if '/etc/cloud/cloud.cfg.d/' in defcfg:
# need to also change datasource, probably
if os.path.exists('/var/lib/cloud/instances/iid-datasource-none/network-config.json'):
os.remove('/var/lib/cloud/instances/iid-datasource-none/network-config.json')
else:
oumask = os.umask(0o77)
with open(defcfg, 'w') as cloudinit:
cloudinit.write(yaml.dump(cloudinfo))
os.umask(oumask)
if needcfgapply:
subprocess.check_call(['netplan', 'generate'])
subprocess.call(['netplan', 'apply'])
def getcfgarrpath(self, devpath):
currptr = self.cfgbydev
currptr = self.cfgbybond if devpath[0] in self.cfgbybond else self.cfgbydev
for k in devpath[:-1]:
if k not in currptr:
currptr[k] = {}
@@ -356,7 +431,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 +563,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
@@ -110,14 +110,22 @@ else
for nic in $(ip link | grep mtu|grep -v LOOPBACK|cut -d: -f 2|sed -e 's/ //'); do
ip link set $nic up
done
for nic in $(ip link | grep mtu|grep -v LOOPBACK|grep LOWER_UP|cut -d: -f 2|sed -e 's/ //'); do
if udhcpc -i $nic -p $dhcpid -t 2 -T 2 -n -x 93:0007 -x 97:00$dhuuid -q; then
/opt/confluent/bin/copernicus > /etc/confluent/confluent.info
if grep ^MANAGER:.*\\. /etc/confluent/confluent.info ; then
break
nic=
while [ -z "$nic" ]; do
for nic in $(ip link | grep mtu|grep -v LOOPBACK|grep LOWER_UP|cut -d: -f 2|sed -e 's/ //'); do
if udhcpc -i $nic -p $dhcpid -t 2 -T 2 -n -x 93:0007 -x 97:00$dhuuid -q; then
/opt/confluent/bin/copernicus > /etc/confluent/confluent.info
if grep ^MANAGER:.*\\. /etc/confluent/confluent.info ; then
break
fi
fi
ip -4 address flush dev $nic
nic=""
done
if [ -z "$nic" ]; then
echo "No network interface could be detected, retrying...."
sleep 2
fi
ip -4 flush dev $nic
done
mgr=$(grep ^MANAGER:.*\\. /etc/confluent/confluent.info|head -n 1|cut -d: -f 2|sed -e 's/ //')
nodename=$(grep ^NODENAME: /etc/confluent/confluent.info|head -n 1|cut -d: -f 2|sed -e 's/ //')
@@ -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
@@ -8,7 +8,9 @@ for addr in $(grep ^MANAGER: /etc/confluent/confluent.info|awk '{print $2}'|sed
fi
done
mkdir -p /mnt/remoteimg /mnt/remote /mnt/overlay
if grep confluent_imagemethod=untethered /proc/cmdline > /dev/null; then
TETHERED=1
if grep -q confluent_imagemethod=untethered /proc/cmdline || grep -q confluent_imagemethod=uncompressed /proc/cmdline; then
TETHERED=0
mount -t tmpfs untethered /mnt/remoteimg
curl https://$confluent_mgr/confluent-public/os/$confluent_profile/rootimg.sfs -o /mnt/remoteimg/rootimg.sfs
else
@@ -44,15 +46,46 @@ 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
modprobe xfs
mkdir /sysroot
mkfs.xfs /dev/zram0 > /dev/null
mount -o discard /dev/zram0 /mnt/overlay
if [ ! -f /tmp/mountparts.sh ]; then
if ! grep -q confluent_imagemethod=uncompressed /proc/cmdline; 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
modprobe xfs
mkfs.xfs /dev/zram0 > /dev/null
if [ "$TETHERED" = 1 ]; then
mount -o discard /dev/zram0 /mnt/overlay
else
mount -o discard /dev/zram0 /sysroot
fi
mount -o discard /dev/zram0 /mnt/overlay
elif grep -q confluent_imagemethod=uncompressed /proc/cmdline; then
mount -t tmpfs disklessroot /sysroot
fi
if [ "$TETHERED" = 0 ]; then
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 -a /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%"
elif [ ! -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
@@ -68,6 +101,7 @@ cp /root/.ssh/* /sysroot/root/.ssh
chmod 700 /sysroot/root/.ssh
cp /etc/confluent/* /sysroot/etc/confluent/
cp /etc/ssh/*key* /sysroot/etc/ssh/
cp /tls/* /sysroot/etc/ssl/certs
for pubkey in /etc/ssh/ssh_host*key.pub; do
certfile=${pubkey/.pub/-cert.pub}
privfile=${pubkey%.pub}
@@ -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
@@ -23,6 +23,7 @@ touch /etc/cloud/cloud-init.disabled
source /etc/confluent/functions
confluent_profile=$(grep ^profile: /etc/confluent/confluent.deploycfg|awk '{print $2}')
export confluent_mgr confluent_profile
run_remote_python confignet
run_remote_parts firstboot.d
run_remote_config firstboot.d
curl --capath /etc/confluent/tls -f -H "CONFLUENT_NODENAME: $nodename" -H "CONFLUENT_APIKEY: $confluent_apikey" -X POST -d "status: complete" https://$confluent_mgr/confluent-api/self/updatestatus
@@ -89,7 +89,6 @@ chroot /target update-ca-certificates
chroot /target bash -c "source /etc/confluent/functions; run_remote_python autoconsole"
chroot /target bash -c "source /etc/confluent/functions; run_remote_python syncfileclient"
chroot /target bash -c "source /etc/confluent/functions; run_remote_python confignet"
chroot /target bash -c "source /etc/confluent/functions; run_remote_parts post.d"
source /target/etc/confluent/functions
@@ -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 {
+76 -72
View File
@@ -213,97 +213,101 @@ int main(int argc, char* argv[]) {
memset(msg, 0, 1024);
/* Deny packet access to the last 24 bytes to assure null */
recvfrom(n4, msg, 1000, 0, (struct sockaddr *)&dst4, &dst4size);
if (nodenameidx = strstr(msg, "NODENAME: ")) {
if (strstr(msg, "HTTP/1.1 200 OK")) {
if (nodenameidx = strstr(msg, "NODENAME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
nodenameidx = strstr(nodename, "\r");
if (nodenameidx) { nodenameidx[0] = 0; }
if (strncmp(lastnodename, nodename, 1024) != 0) {
printf("NODENAME: %s\n", nodename);
strncpy(lastnodename, nodename, 1024);
}
}
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
nodenameidx = strstr(nodename, "\r");
if (nodenameidx) { nodenameidx[0] = 0; }
if (strncmp(lastnodename, nodename, 1024) != 0) {
printf("NODENAME: %s\n", nodename);
strncpy(lastnodename, nodename, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
}
}
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
settime = strtol(nodename, NULL, 10);
}
settime = strtol(nodename, NULL, 10);
}
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
}
setusec = strtol(nodename, NULL, 10) * 1000;
}
memset(msg, 0, 1024);
inet_ntop(dst4.sin_family, &dst4.sin_addr, msg, dst4size);
/* Take measure from printing out the same ip twice in a row */
if (strncmp(lastmsg, msg, 1024) != 0) {
sendto(n4, "PING", 4, 0, (const struct sockaddr *)&dst4, dst4size);
printf("MANAGER: %s\n", msg);
strncpy(lastmsg, msg, 1024);
}
setusec = strtol(nodename, NULL, 10) * 1000;
}
memset(msg, 0, 1024);
inet_ntop(dst4.sin_family, &dst4.sin_addr, msg, dst4size);
/* Take measure from printing out the same ip twice in a row */
if (strncmp(lastmsg, msg, 1024) != 0) {
sendto(n4, "PING", 4, 0, (const struct sockaddr *)&dst4, dst4size);
printf("MANAGER: %s\n", msg);
strncpy(lastmsg, msg, 1024);
}
}
if (FD_ISSET(ns, &rfds)) {
memset(msg, 0, 1024);
/* Deny packet access to the last 24 bytes to assure null */
recvfrom(ns, msg, 1000, 0, (struct sockaddr *)&dst, &dstsize);
if (nodenameidx = strstr(msg, "NODENAME: ")) {
if (strstr(msg, "HTTP/1.1 200 OK")) {
if (nodenameidx = strstr(msg, "NODENAME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
nodenameidx = strstr(nodename, "\r");
if (nodenameidx) { nodenameidx[0] = 0; }
if (strncmp(lastnodename, nodename, 1024) != 0) {
printf("NODENAME: %s\n", nodename);
strncpy(lastnodename, nodename, 1024);
}
}
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
nodenameidx = strstr(nodename, "\r");
if (nodenameidx) { nodenameidx[0] = 0; }
if (strncmp(lastnodename, nodename, 1024) != 0) {
printf("NODENAME: %s\n", nodename);
strncpy(lastnodename, nodename, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
}
}
if (nodenameidx = strstr(msg, "CURRTIME: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
settime = strtol(nodename, NULL, 10);
}
settime = strtol(nodename, NULL, 10);
}
if (nodenameidx = strstr(msg, "DEFAULTNET: 1")) {
isdefault = 1;
}
if (nodenameidx = strstr(msg, "MGTIFACE: ")) {
nodenameidx += 10;
strncpy(mgtifname, nodenameidx, 1024);
if (nodenameidx = strstr(mgtifname, "\r")) {
nodenameidx[0] = 0;
if (nodenameidx = strstr(msg, "DEFAULTNET: 1")) {
isdefault = 1;
}
}
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
if (nodenameidx = strstr(msg, "MGTIFACE: ")) {
nodenameidx += 10;
strncpy(mgtifname, nodenameidx, 1024);
if (nodenameidx = strstr(mgtifname, "\r")) {
nodenameidx[0] = 0;
}
}
setusec = strtol(nodename, NULL, 10) * 1000;
}
memset(msg, 0, 1024);
inet_ntop(dst.sin6_family, &dst.sin6_addr, msg, dstsize);
if (strncmp(last6msg, msg, 1024) != 0 || lastidx != dst.sin6_scope_id) {
lastidx = dst.sin6_scope_id;
sendto(ns, "PING", 4, 0, (const struct sockaddr *)&dst, dstsize);
printf("MANAGER: %s", msg);
if (strncmp(msg, "fe80::", 6) == 0) {
printf("%%%u", dst.sin6_scope_id);
if (nodenameidx = strstr(msg, "CURRMSECS: ")) {
nodenameidx += 10;
strncpy(nodename, nodenameidx, 1024);
if (nodenameidx = strstr(nodename, "\r")) {
nodenameidx[0] = 0;
}
setusec = strtol(nodename, NULL, 10) * 1000;
}
printf("\n");
printf("EXTMGRINFO: %s", msg);
if (strncmp(msg, "fe80::", 6) == 0) {
printf("%%%u", dst.sin6_scope_id);
memset(msg, 0, 1024);
inet_ntop(dst.sin6_family, &dst.sin6_addr, msg, dstsize);
if (strncmp(last6msg, msg, 1024) != 0 || lastidx != dst.sin6_scope_id) {
lastidx = dst.sin6_scope_id;
sendto(ns, "PING", 4, 0, (const struct sockaddr *)&dst, dstsize);
printf("MANAGER: %s", msg);
if (strncmp(msg, "fe80::", 6) == 0) {
printf("%%%u", dst.sin6_scope_id);
}
printf("\n");
printf("EXTMGRINFO: %s", msg);
if (strncmp(msg, "fe80::", 6) == 0) {
printf("%%%u", dst.sin6_scope_id);
}
printf("|%s|%d\n", mgtifname, isdefault);
strncpy(last6msg, msg, 1024);
}
printf("|%s|%d\n", mgtifname, isdefault);
strncpy(last6msg, msg, 1024);
}
}
}
+2 -2
View File
@@ -132,8 +132,8 @@ static int http_read(const char *path, char *buf, size_t size, off_t offset,
if (strcmp(path, filename) != 0) return -ENOENT;
memset(headbuffer, 0, 512);
if (offset >= filesize) return 0;
if (offset + size - 1 >= filesize) size = filesize - offset - 1;
if (offset >= filesize || size == 0) return 0;
if (offset + size > filesize) size = filesize - offset;
snprintf(headbuffer, 512, "%ld-%ld", offset, offset + size - 1);
if (curl_easy_setopt(curl, CURLOPT_RANGE, headbuffer) != CURLE_OK) {
fprintf(stderr, "Error setting range\n");
+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.
@@ -379,7 +379,7 @@ node = {
'the managed node. If not specified, then console '
'is disabled. "ipmi" should be specified for most '
'systems if console is desired.'),
'validvalues': ('ssh', 'ipmi', 'openbmc', 'tsmsol', 'vcenter', 'proxmox'),
'validvalues': ('ssh', 'ipmi', 'openbmc', 'megasol', 'tsmsol', 'vcenter', 'proxmox'),
},
# 'virtualization.host': {
# 'description': ('Hypervisor where this node does/should reside'),
@@ -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")
@@ -1306,7 +1382,7 @@ class ConfigManager(object):
return _cfgstore['main']
return _cfgstore['tenant'][self.tenant]
def __init__(self, tenant, decrypt=False, username=None):
def __init__(self, tenant, decrypt=False, username=None, create_tenant=False):
self.clientfiles = {}
global _cfgstore
self.inrestore = False
@@ -1328,12 +1404,14 @@ class ConfigManager(object):
self._cfgstore['nodes'] = {}
self._bg_sync_to_file()
return
elif 'tenant' not in _cfgstore:
elif 'tenant' not in _cfgstore and create_tenant:
_cfgstore['tenant'] = {tenant: {}}
self._bg_sync_to_file()
elif tenant not in _cfgstore['tenant']:
elif tenant not in _cfgstore['tenant'] and create_tenant:
_cfgstore['tenant'][tenant] = {}
self._bg_sync_to_file()
elif tenant and tenant not in _cfgstore.get('tenant', {}):
raise ValueError("Tenant {0} does not exist".format(tenant))
self.tenant = tenant
if 'nodegroups' not in self._cfgstore:
self._cfgstore['nodegroups'] = {'everything': {}}
@@ -1344,6 +1422,9 @@ class ConfigManager(object):
self.wait_for_sync()
def add_client_file(self, clientfile):
filename = os.path.normpath(clientfile.filename)
if filename.startswith('../') or filename.startswith('..\\'):
raise ValueError("Invalid filename: {0}".format(clientfile.filename))
self.clientfiles[clientfile.filename] = clientfile.fileobject
def close_client_files(self):
@@ -2206,7 +2287,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 +2295,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 +2595,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 +3250,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 +3285,7 @@ def init(stateless=False):
_cfgstore = {}
members = list(list_collective())
if len(members) < 2:
_init_indexes()
_ready = True
+18 -3
View File
@@ -61,6 +61,8 @@ def chunk_output(output, n):
yield output[i:i + n]
def get_buffer_output(nodename):
if _bufferdaemon is None:
eventlet.spawn(run_buffer_daemon)
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
out.connect("\x00confluent-vtbuffer")
@@ -85,6 +87,8 @@ def get_buffer_output(nodename):
def send_output(nodename, output):
if not isinstance(nodename, bytes):
nodename = nodename.encode('utf8')
if _bufferdaemon is None:
eventlet.spawn(run_buffer_daemon)
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
out.connect("\x00confluent-vtbuffer")
@@ -597,14 +601,25 @@ def _start_tenant_sessions(cfm):
event=log.Events.stacktrace)
cfm.watch_nodecollection(_nodechange)
running = True
def run_buffer_daemon():
global _bufferdaemon
while running:
minrestartdeadline = time.time() + 30 # Do not restart more than once every 30 seconds
_bufferdaemon = subprocess.Popen(
['/opt/confluent/bin/vtbufferd', 'confluent-vtbuffer'], bufsize=0, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL)
_bufferdaemon.wait()
# Ensure we do not restart more than once every 30 seconds
sleep_time = minrestartdeadline - time.time()
if sleep_time > 0:
eventlet.sleep(sleep_time)
def initialize():
global _tracelog
global _bufferdaemon
_tracelog = log.Logger('trace')
_bufferdaemon = subprocess.Popen(
['/opt/confluent/bin/vtbufferd', 'confluent-vtbuffer'], bufsize=0, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL)
def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
+34 -13
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': {
@@ -1368,17 +1390,16 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata):
'''
if operation == 'create':
if len(pathcomponents) == 1:
stage = Staging(inputdata['user'],str(uuid.uuid1()))
if stage.create_directory():
if 'filename' in inputdata:
data_file = stage.storage_folder + '/filename.txt'
with open(data_file, 'w') as f:
f.write(inputdata['filename'])
else:
raise Exception('Error: Missing filename arg')
push_url = stage.get_push_url()
yield msg.CreatedResource(push_url)
if 'filename' not in inputdata:
raise Exception('Error: Missing filename parameter')
inputdata['filename'] = os.path.normpath(inputdata['filename'])
if '/' in inputdata['filename'] or '\\' in inputdata['filename']:
raise Exception('Error: Invalid filename parameter, must not contain path separators')
stage = Staging(inputdata['user'],str(uuid.uuid4()))
if stage.create_directory():
data_file = stage.storage_folder + '/filename.txt'
push_url = stage.get_push_url()
yield msg.CreatedResource(push_url)
elif len(pathcomponents) == 3:
stage = Staging(pathcomponents[1], pathcomponents[2])
file = stage.get_file_name()
+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(
@@ -174,6 +174,7 @@ pxearchs = {
b'\x00\x09': 'uefi-x64',
b'\x00\x0b': 'uefi-aarch64',
b'\x00\x10': 'uefi-httpboot',
b'\x00\x13': 'uefi-httpboot', # arm httpboot
}
@@ -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']
+23 -15
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:
@@ -360,11 +361,12 @@ def _authorize_request(env, operation, reqbody):
return {'code': 401}
sessid = _establish_http_session(env, authdata, name, cookie)
if authdata and element and element.startswith('/sessions/current/webauthn/validate/'):
if webauthn:
for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None):
if rsp['verified']:
sessid = _establish_http_session(env, authdata, name, cookie)
break
if not webauthn:
return {'code': 501}
for rsp in webauthn.handle_api_request(element, env, None, authdata[2], authdata[1], None, reqbody, None):
if rsp['verified']:
sessid = _establish_http_session(env, authdata, name, cookie)
break
skiplog = _should_skip_authlog(env)
if authdata:
auditmsg = {
@@ -846,7 +848,7 @@ def resourcehandler_backend(env, start_response):
yield 'Our princess is in another castle!'
return
elif (operation == 'create' and ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO'])):
'/shell/sessions/' in env['PATH_INFO']) and env['PATH_INFO'].startswith(('/nodes/', '/noderange/'))):
#hard bake JSON into this path, do not support other incarnations
if '/console/session' in env['PATH_INFO']:
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
@@ -984,9 +986,7 @@ def resourcehandler_backend(env, start_response):
start_response('200 OK', headers)
yield rsp
return
elif (operation == 'create' and ('/firmware/updates/active' in env['PATH_INFO'])):
elif (operation == 'create' and ('/firmware/updates/active' in env['PATH_INFO']) and env['PATH_INFO'].startswith(('/nodes/', '/noderange/'))):
url = env['PATH_INFO']
if 'application/json' in reqtype:
if not isinstance(reqbody, str):
@@ -1007,8 +1007,7 @@ def resourcehandler_backend(env, start_response):
yield json.dumps({'data': nodeurls})
start_response('200 OK', headers)
return
elif (operation == 'create' and ('/staging' in env['PATH_INFO'])):
elif (operation == 'create' and (env['PATH_INFO'].startswith('/staging'))):
url = env['PATH_INFO']
args_dict = {}
content_length = int(env.get('CONTENT_LENGTH', 0))
@@ -1217,7 +1216,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 +1231,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 +1267,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')
+98 -1
View File
@@ -19,6 +19,7 @@
# Things are defined here to 'encourage' developers to coordinate information
# format. This is also how different data formats are supported
import base64
import os
import confluent.exceptions as exc
import confluent.config.configmanager as cfm
import confluent.config.conf as cfgfile
@@ -27,6 +28,7 @@ from datetime import datetime
import confluent.util as util
import msgpack
import json
import pwd
try:
unicode
@@ -519,6 +521,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']:
@@ -567,6 +571,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputPowerMessage(path, nodes, inputdata)
elif '/'.join(path).startswith('media/detach'):
return DetachMedia(path, nodes, inputdata)
elif '/'.join(path).startswith('media/attach') and inputdata:
return InputMediaUrl(path, nodes, inputdata, configmanager)
elif '/'.join(path).startswith('media/') and inputdata:
return InputMedia(path, nodes, inputdata, configmanager)
elif '/'.join(path).startswith('support/servicedata') and inputdata:
@@ -578,6 +584,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']:
@@ -586,19 +594,75 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
raise exc.InvalidArgumentException(
'No known input handler for request')
class InputFirmwareUpdate(ConfluentMessage):
def checkaccess(user, filename, pwent):
"""Check if a user has read access to a file.
This function checks if the specified user has read access to the given
filename. It returns True if the user has read access, and False otherwise.
"""
child = os.fork()
if child == 0:
os.setgroups(os.getgrouplist(user, pwent.pw_gid))
os.setgid(pwent.pw_gid)
os.setuid(pwent.pw_uid)
if os.access(filename, os.R_OK):
os._exit(0)
os._exit(1)
else:
pid, status = os.waitpid(child, 0)
if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
return True
return False
def isurl(value):
prefix, value = value.split('://', 1) if '://' in value else ('', value)
if '/' in prefix:
return False
return True if prefix else False
class InputFirmwareUpdate(ConfluentMessage):
urlsupported = False
def __init__(self, path, nodes, inputdata, configmanager):
self._filename = inputdata.get('filename', inputdata.get('url', inputdata.get('dirname', None)))
self.bank = inputdata.get('bank', None)
self.parameterdata = inputdata.get('parameterdata', None)
self.nodes = nodes
self.filebynode = {}
self._complexname = False
curruser = configmanager.current_user if configmanager else None
# for configmanager filehandles, those are already opened by client, so
# no need to check server side access
checkedfiles = set(list(configmanager.clientfiles))
for expanded in configmanager.expand_attrib_expression(
nodes, self._filename):
node, value = expanded
if value != self._filename:
self._complexname = True
if self.urlsupported and isurl(value):
self.filebynode[node] = value
continue
value = os.path.normpath(value)
if value not in checkedfiles:
if value.startswith('../'):
raise Exception('File transfer with ../ is not supported')
if value.startswith('/etc/confluent'):
raise Exception(
'File transfer with /etc/confluent is not supported')
if value.startswith('/var/log/confluent'):
raise Exception(
'File transfer with /var/log/confluent is not supported')
if curruser and not value.startswith('/var/lib/confluent/client_assets/'):
try:
pwent = pwd.getpwnam(curruser)
if not checkaccess(curruser, value, pwent):
errstr = '{0} is not readable by {1}, check the file and parent directory ownership and permissions'.format(
value, curruser)
raise Exception(errstr)
except KeyError:
pass # We can't check ownership for confluent users without system users, as is the case in a prominent container usage,
# We must rely upon the banned paths to mitigate risk instead
checkedfiles.add(value)
self.filebynode[node] = value
@property
@@ -628,6 +692,11 @@ class InputMedia(InputFirmwareUpdate):
# Use InputFirmwareUpdate
pass
class InputMediaUrl(InputFirmwareUpdate):
# Use InputFirmwareUpdate for URL-based media attachment
urlsupported = True
pass
class InputLicense(InputFirmwareUpdate):
pass
@@ -721,6 +790,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 +1028,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 +1061,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',
@@ -1257,6 +1347,7 @@ class BootDevice(ConfluentChoiceMessage):
'cd',
'floppy',
'usb',
'http',
])
valid_bootmodes = set([
@@ -1699,12 +1790,18 @@ class Disk(ConfluentMessage):
'rebuilding',
'online',
'offline',
'failed',
'foreign',
])
state_aliases = {
'unconfigured bad': 'fault',
'unconfigured good': 'unconfigured',
'(foreign) unconfigured good': 'foreign',
'unconfiguredgood': 'unconfigured',
'global hot spare': 'hotspare',
'globalhotspare': 'hotspare',
'dedicated hot spare': 'hotspare',
'dedicatedhotspare': 'hotspare',
}
def _normalize_state(self, instate):
+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,160 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This plugin provides an ssh implementation comforming to the 'console'
# specification. consoleserver or shellserver would be equally likely
# to use this.
import confluent.exceptions as cexc
import confluent.interface.console as conapi
import confluent.log as log
import confluent.util as util
import pyghmi.exceptions as pygexc
import pyghmi.redfish.command as rcmd
import eventlet
import eventlet.green.ssl as ssl
try:
websocket = eventlet.import_patched('websocket')
wso = websocket.WebSocket
except Exception:
wso = object
def get_conn_params(node, configdata):
if 'secret.hardwaremanagementuser' in configdata:
username = configdata['secret.hardwaremanagementuser']['value']
else:
username = 'USERID'
if 'secret.hardwaremanagementpassword' in configdata:
passphrase = configdata['secret.hardwaremanagementpassword']['value']
else:
passphrase = 'PASSW0RD' # for lack of a better guess
if 'hardwaremanagement.manager' in configdata:
bmc = configdata['hardwaremanagement.manager']['value']
else:
bmc = node
bmc = bmc.split('/', 1)[0]
return {
'username': username,
'passphrase': passphrase,
'bmc': bmc,
}
_configattributes = ('secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager')
class WrappedWebSocket(wso):
def set_verify_callback(self, callback):
self._certverify = callback
def connect(self, url, **options):
add_tls = url.startswith('wss://')
if add_tls:
hostname, port, resource, _ = websocket._url.parse_url(url)
if hostname[0] != '[' and ':' in hostname:
hostname = '[{0}]'.format(hostname)
if resource[0] != '/':
resource = '/{0}'.format(resource)
url = 'ws://{0}:443{1}'.format(hostname,resource)
else:
return super(WrappedWebSocket, self).connect(url, **options)
self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)
self.sock, addrs = websocket._http.connect(url, self.sock_opt, websocket._http.proxy_info(**options),
options.pop('socket', None))
self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE)
# The above is supersedeed by the _certverify, which provides
# known-hosts style cert validaiton
bincert = self.sock.getpeercert(binary_form=True)
if not self._certverify(bincert):
raise pygexc.UnrecognizedCertificate('Unknown certificate', bincert)
try:
self.handshake_response = websocket._handshake.handshake(self.sock, url, *addrs, **options)
if self.handshake_response.status in websocket._handshake.SUPPORTED_REDIRECT_STATUSES:
options['redirect_limit'] = options.pop('redirect_limit', 3) - 1
if options['redirect_limit'] < 0:
raise Exception('Redirect limit hit')
url = self.handshake_response.headers['location']
self.sock.close()
return self.connect(url, **options)
self.connected = True
except:
if self.sock:
self.sock.close()
self.sock = None
raise
class TsmConsole(conapi.Console):
def __init__(self, node, config):
self.node = node
self.ws = None
configdata = config.get_node_attributes([node], _configattributes, decrypt=True)
connparams = get_conn_params(node, configdata[node])
self.username = connparams['username']
self.password = connparams['passphrase']
self.bmc = connparams['bmc']
self.origbmc = connparams['bmc']
if ':' in self.bmc:
self.bmc = '[{0}]'.format(self.bmc)
self.datacallback = None
self.nodeconfig = config
self.connected = False
def recvdata(self):
while self.connected:
pendingdata = self.ws.recv()
if pendingdata == '':
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
self.datacallback(pendingdata)
def connect(self, callback):
self.datacallback = callback
rc = rcmd.Command(self.origbmc, self.username,
self.password,
verifycallback=lambda x: True)
wc = rc.oem.wc
bmc = self.bmc
if '%' in self.bmc:
prefix = self.bmc.split('%')[0]
bmc = prefix + ']'
self.ws = WrappedWebSocket(host=bmc)
kv = util.TLSCertVerifier(
self.nodeconfig, self.node, 'pubkeys.tls_hardwaremanager').verify_cert
self.ws.set_verify_callback(kv)
self.ws.connect('wss://{0}/h5sol'.format(self.bmc), host=bmc, cookie='QSESSIONID={0}; __Host-garc={1}'.format(wc.cookies['QSESSIONID'], rc.oem.csrftok))
self.connected = True
eventlet.spawn_n(self.recvdata)
return
def write(self, data):
self.ws.send(data)
def close(self):
if self.ws:
self.ws.close()
self.connected = False
self.datacallback = None
def create(nodes, element, configmanager, inputdata):
if len(nodes) == 1:
return TsmConsole(nodes[0], configmanager)
@@ -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)
@@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import confluent.vinzmanager as vinzmanager
import confluent.exceptions as exc
import confluent.firmwaremanager as firmwaremanager
@@ -20,6 +22,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 +40,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 +188,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)
@@ -480,8 +488,16 @@ class IpmiHandler(object):
else:
raise Exception('Not Implemented')
def update_firmware(self, filename, progress, data, bank):
params=()
if self.inputdata.parameterdata:
params = self.inputdata.parameterdata
if params and isinstance(params, str):
params = json.loads(params)
return self.ipmicmd.update_firmware(filename, progress=progress, data=data, bank=bank, otherfields=params)
def handle_update(self):
u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
u = firmwaremanager.Updater(self.node, self.update_firmware,
self.inputdata.nodefile(self.node), self.tenant,
bank=self.inputdata.bank,
configmanager=self.cfm)
@@ -528,6 +544,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 +596,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

Some files were not shown because too many files have changed in this diff Show More