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

Compare commits

..

402 Commits

Author SHA1 Message Date
Jarrod Johnson ec1efecdae Merge branch 'master' into async 2026-04-14 13:51:36 -04:00
Jarrod Johnson c54ac530e1 Handle some environments where timedatectl does not exist 2026-04-14 13:50:12 -04:00
Jarrod Johnson 8db76b92ee Fix update of pinned cert on CA blessing 2026-04-14 10:53:34 -04:00
Jarrod Johnson 2a32fc85a6 Skip policy setting for now and take defaults. 2026-04-14 10:45:45 -04:00
Jarrod Johnson 2bd13c397d Rework for older python cryptography compatibility 2026-04-14 10:45:03 -04:00
Jarrod Johnson 5250a3a67a Pass subject to the verifier in redfish 2026-04-14 10:24:33 -04:00
Jarrod Johnson 038faaab74 Await clear node attributes 2026-04-14 09:57:33 -04:00
Jarrod Johnson a8f4c437bb Remove duplicate copy of function 2026-04-13 16:12:30 -04:00
Jarrod Johnson 9d17102f60 Await creation of the certificate 2026-04-13 16:05:55 -04:00
Jarrod Johnson f2ce13253f Properly place messages on async queue 2026-04-13 13:17:09 -04:00
Jarrod Johnson 65944a4507 For fixes for sync use of async methods 2026-04-13 13:07:10 -04:00
Jarrod Johnson 7c8cee2480 Certificate list fix 2026-04-13 12:46:04 -04:00
Jarrod Johnson 31a56f9fdc Merge branch 'master' into async 2026-04-08 16:17:23 -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 131fa052e0 Rework merge to be async friendly 2026-04-08 15:13:42 -04:00
Jarrod Johnson f5a1c0d1b4 Merge branch 'master' into async 2026-04-08 15:08:30 -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 9dad93fd26 Improve raritan support by persisting info about a pdu
This gets the time for a re-sweep of sensors under a second.

It's come a long way from the 3 minutes it was taking.
2026-04-06 11:40:34 -04:00
Jarrod Johnson f125ff0fb6 Implement session management for raritan
The session token accelerates requests.  Shaves a second off of a sensor pass
2026-04-06 11:02:04 -04:00
Jarrod Johnson ec6cdd6c21 Defer inlet sensor to shave a few more seconds from a sensor sweep. 2026-04-06 10:12:18 -04:00
Jarrod Johnson c53206331c Leverage bulk facility
Study of the web interface showed that bulk requests are a key component.

This takes a sensor sweep from about 3 minutes to about 7 seconds in a test environment.
2026-04-06 10:09:10 -04:00
Jarrod Johnson 7bbc451047 Draft raritan pdu support
Particularly need to replace the sensors logic to provide vaguely credible performance
2026-04-03 11:00:23 -04:00
Jarrod Johnson 5ccbc37aa6 Merge branch 'master' into async 2026-04-03 10:36:25 -04:00
Jarrod Johnson 69d984b9dc Fix syntax mistake in deferred handling in nodeapply 2026-04-03 10:34:20 -04:00
Jarrod Johnson dade4239aa Fix user management with redfish 2026-04-03 08:37:05 -04:00
Jarrod Johnson 6d17d9f0f8 Fix some storage api calls under ipmi 2026-04-02 16:33:35 -04:00
Jarrod Johnson 62e9ee8dac Fix user password manipulation in ipmi 2026-04-02 15:52:54 -04:00
Jarrod Johnson 326b659d28 Merge branch 'master' into async 2026-04-02 15:29:49 -04:00
Jarrod Johnson a123165712 Improve error when unknown user specified in syncfiles 2026-04-02 15:29:31 -04:00
Jarrod Johnson 8c9d55d469 Fix nodesetboot 2026-03-27 17:11:35 -04:00
Jarrod Johnson a1e7cb1e9d Fix nodeinventory for vertiv pdus 2026-03-27 17:00:17 -04:00
Jarrod Johnson 7f4da79679 Fix taskpile to work as intended 2026-03-27 16:35:06 -04:00
Jarrod Johnson fecf766183 Change to returning a list of msgs in enlogic 2026-03-27 13:02:35 -04:00
Jarrod Johnson f842a0a6e3 Fikup TaskPile and it's uses 2026-03-27 12:56:47 -04:00
Jarrod Johnson 2462e07832 Fix async gen invocation of apply license 2026-03-26 16:57:46 -04:00
Jarrod Johnson 939d5c59ea Fix async apply license in ipmi plugin 2026-03-26 16:48:37 -04:00
Jarrod Johnson 36d3cdbe41 Merge branch 'master' into async 2026-03-25 13:00:02 -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 d44e7f0955 Port some license management to async 2026-03-24 16:36:03 -04:00
Jarrod Johnson cd68225672 Fix nodeconfig and nodefirmware to IPMI targets 2026-03-24 16:23:48 -04:00
Jarrod Johnson f18cc981e2 Fix async discovery of XCC 2026-03-24 15:41:48 -04:00
Jarrod Johnson 8c482d9078 Numerous async modifications for XCC1/2 discovery 2026-03-24 14:48:05 -04:00
Jarrod Johnson 78c708424d async fixes for nodeconfig and netutil 2026-03-24 14:08:54 -04:00
Jarrod Johnson 9b6ead31a7 Consume async generator 2026-03-24 08:29:40 -04:00
Jarrod Johnson 8e508ee858 Properly await async pushes to async queue 2026-03-20 16:24:35 -04:00
Jarrod Johnson 7f80a4d5aa Port enhancements from sync client to async 2026-03-20 16:11:26 -04:00
Jarrod Johnson 315fb0ced8 Port more methodns to await 2026-03-20 15:59:00 -04:00
Jarrod Johnson 6dc57abe28 Numerous async changes
For one, simplify and make more robust normalizing iterating various things.

Add required async/await in varous other places.
2026-03-20 15:09:11 -04:00
Jarrod Johnson 36bf03d65c Fix passing of args in handoff to async 2026-03-20 10:56:18 -04:00
Jarrod Johnson 53c2ace620 Go all in on systemd startup
Become a notify type service.  Reserve the ability to be normal should it come up, but with
the complications around fork, easiest
to punt on daemonize for now.
2026-03-20 10:12:45 -04:00
Jarrod Johnson bec5363877 Merge branch 'master' into async 2026-03-19 18:05:50 -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 d8c7e1fc2a Add a note why fork might be ok in auth 2026-03-19 17:28:51 -04:00
Jarrod Johnson 7adef74fe9 Delay async until after daemonize
os.fork shouldn't happen after an async event loop if the child might use the async loop
2026-03-19 17:27:24 -04:00
Jarrod Johnson 40da956a06 Fix confluent_selfcheck for asyncio
Most dramatically, rework to avoid os.fork, which
ruins threading and by extension the getaddrinfo behavior.
2026-03-19 17:21:30 -04:00
Jarrod Johnson 07a6eb32ed Merge branch 'master' into async 2026-03-19 12:21:10 -04:00
Jarrod Johnson f78b301143 Update usage text 2026-03-19 09:54:08 -04:00
Jarrod Johnson 57fe186a10 Rework redfish to be more async 2026-03-17 16:43:41 -04:00
Jarrod Johnson b4c4ac0861 Fix async syncfiles 2026-03-17 16:10:21 -04:00
Jarrod Johnson 2ab85bb687 Refactor functions to be a bit more readable 2026-03-17 15:40:01 -04:00
Jarrod Johnson e1a6a1c9bf Merge branch 'master' into async 2026-03-17 13:03:56 -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 a6e7d016ea Restore ansible running in async, complete with recent changes from master 2026-03-13 14:57:59 -04:00
Jarrod Johnson 52db46be93 Fix python detection from ansible with space in shebang 2026-03-13 11:41:16 -04:00
Jarrod Johnson 2c8cce74ab Fix some issues from async ansible running 2026-03-13 11:40:44 -04:00
Jarrod Johnson fed83841bb Merge branch 'master' into async 2026-03-13 09:26:20 -04:00
Jarrod Johnson 550dfbf6a0 Fix reference of inputdata in remoteconfig 2026-03-13 09:26:11 -04:00
Jarrod Johnson 89cc70260f Merge branch 'master' into async 2026-03-13 08:59:15 -04:00
Jarrod Johnson e0951b11a6 Fix filename typo 2026-03-13 08:58:58 -04:00
Jarrod Johnson 794502eb6a Merge branch 'master' into async 2026-03-09 17:30:04 -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 a4510ae58d Fix ordering of width/height geometry 2026-03-04 16:54:27 -05:00
Jarrod Johnson a7c5b2478c Get screenshots working in redfish asyncio 2026-03-04 16:04:51 -05:00
Jarrod Johnson 14035fce88 Implement fallback for screen geometry
Ideally, we can do TIOCGWINSZ.

Unfortunately, in some cases this breaks, resort to
escape codes.
2026-03-04 16:04:08 -05:00
Jarrod Johnson 42bfde3f86 Restore VNC console handling to async branch 2026-03-04 15:13:30 -05:00
Jarrod Johnson 2c75571a84 Fixes to allow a test deployment to complete under asyncio 2026-03-04 10:47:33 -05:00
Jarrod Johnson a5e7fe93e4 Fix ip address list 2026-03-03 17:12:28 -05:00
Jarrod Johnson bfb41de43b Further asyncio conversion work 2026-03-03 16:06:14 -05:00
Jarrod Johnson c44cdc21ea Fix async definition of decode_alert 2026-03-03 14:53:54 -05:00
Jarrod Johnson c806bf2234 Rework more of ipmi support for async 2026-03-03 14:49:21 -05:00
Jarrod Johnson 61ada4d3d4 Advance async rework of ipmi 2026-03-03 13:05:10 -05:00
Jarrod Johnson f10a173e62 Address numerous reworks of async 2026-03-03 12:50:32 -05:00
Jarrod Johnson 27a3a446fe Fix issues in nodeconfig async 2026-03-03 12:42:29 -05:00
Jarrod Johnson 9abe1f98b7 Numerous await changes for ipmi 2026-03-03 12:42:17 -05:00
Jarrod Johnson 1fd986cc9a async fixes for ipmi 2026-03-02 16:43:12 -05:00
Jarrod Johnson f97c481c62 Normalize various iterables for single node responses 2026-03-02 13:01:28 -05:00
Jarrod Johnson d50fa2b587 Handle some async conversions 2026-03-02 12:17:35 -05:00
Jarrod Johnson a31532d8e4 Fix transmit of notfound errors 2026-03-02 08:41:18 -05:00
Jarrod Johnson c8fcb716fb Fix socket being in blocking mode before async calls. 2026-03-02 08:33:34 -05:00
Jarrod Johnson 289a6a6af8 Adjustments to purge eventlet references in deltapdu 2026-03-02 08:30:12 -05:00
Jarrod Johnson 6379b03051 Purge eventlet from plugins 2026-03-01 13:42:36 -05:00
Jarrod Johnson 580dd283cc Fix some async gaps 2026-02-27 13:37:32 -05:00
Jarrod Johnson 9d85a3c993 Remove eventlet from dependencies 2026-02-27 13:32:08 -05:00
Jarrod Johnson 3636e14628 Rework netutil for async and dependent functions 2026-02-26 17:27:04 -05:00
Jarrod Johnson 84f3614f0a Convert vcenter console to async 2026-02-26 13:16:17 -05:00
Jarrod Johnson 97f1545f97 Initial wave of vcenter async conversion 2026-02-26 12:57:15 -05:00
Jarrod Johnson e6bcf3cf9a Fix proxmox console for async operation 2026-02-26 10:49:33 -05:00
Jarrod Johnson 61c063adf4 Draft of converting tsmsol to asyncio 2026-02-25 16:19:05 -05:00
Jarrod Johnson 36898ba570 Fix openbmc console method with async 2026-02-25 16:14:51 -05:00
Jarrod Johnson 2f2afd970a Merge branch 'master' into async 2026-02-25 10:02:05 -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 f2de24015b Port mdns to the async architecture similar to ssdp 2026-02-20 10:17:24 -05:00
Jarrod Johnson 9b2eaa90b5 Remove a number of eventlet imports/comments 2026-02-20 10:17:10 -05:00
Jarrod Johnson 16ff57dcfc Fixes for macmap in async 2026-02-20 09:43:34 -05:00
Jarrod Johnson 7adea90169 Fixes for async snmp and SRLinux 2026-02-20 09:29:03 -05:00
Jarrod Johnson f28e6d32f3 Handle NXOS async and advance state of lldp/mac map with async 2026-02-20 08:58:44 -05:00
Jarrod Johnson 09089befc8 SRLinux async handling advancement 2026-02-19 16:34:36 -05:00
Jarrod Johnson 4173356a70 Fixes for srlinux asyncio 2026-02-19 16:25:37 -05:00
Jarrod Johnson ca0c89aa07 Move SRLinux support to asyncio style 2026-02-19 16:08:32 -05:00
Jarrod Johnson ce2487dcb8 Fix import of srlinux 2026-02-19 14:58:31 -05:00
Jarrod Johnson 5b6bd4fbe1 Fix async mistakes from merge 2026-02-19 14:53:28 -05:00
Jarrod Johnson 18d06409d4 Merge branch 'master' into async 2026-02-18 16:54:46 -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 e0f00d80ed Merge branch 'master' into async 2026-02-17 16:19:51 -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 7efbec7ea2 Merge branch 'master' into async 2026-02-11 11:35:31 -05:00
Jarrod Johnson ef7d2414ad Update nodeconfig usage material 2026-02-11 11:35:09 -05:00
Jarrod Johnson 21867a60d2 Merge branch 'master' into async 2026-02-11 10:55:07 -05:00
Jarrod Johnson 722a0b874a Add notation about certificate and nodemedia 2026-02-11 10:54:30 -05:00
Jarrod Johnson f77c1e9333 Merge branch 'master' into async 2026-02-10 17:10:38 -05:00
Jarrod Johnson 1deb76989e Recognize 1a/2b style enclosure bay in discovery 2026-02-10 17:10:18 -05:00
Jarrod Johnson 9e5c69c286 Merge branch 'master' into async 2026-02-09 13:19:50 -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 25ea00d6d1 Merge branch 'master' into async 2026-02-05 07:58:15 -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 137c3a0688 More async changes for confluent 2026-02-04 16:12:36 -05:00
Jarrod Johnson 00136f61fe More async fixes to remote media related redfish 2026-02-04 15:44:04 -05:00
Jarrod Johnson f9e898a46a Asyncio fixes
Fix ability to receive file descriptions from a unix domain client

Correct invocations to clearbuffer in consoleserver
2026-02-04 15:34:38 -05:00
Jarrod Johnson 06c2299b10 Adjust redfish fetch of diagnostic data 2026-02-04 10:19:13 -05:00
Jarrod Johnson a003ad6e2c Address asyncio changes for consoleserver 2026-02-04 10:02:17 -05:00
Jarrod Johnson d5c85cdff9 Fix async bind handling 2026-02-04 09:51:10 -05:00
Jarrod Johnson cdc668d717 Fix async assumption about list_updates
Turns out that the firmwaremanagemer methods will
generally not be async after all.
2026-02-03 16:43:25 -05:00
Jarrod Johnson 9ea971d9df Begin work to rework firmwaremanager for async
Requires a pool concept to manage concurrent task execution to match
previous expectations.
2026-02-03 16:36:41 -05:00
Jarrod Johnson d2a13f93f3 Fix error handling flow for async in redfish 2026-02-03 11:49:18 -05:00
Jarrod Johnson 850793a73f Merge branch 'master' into async 2026-02-03 07:58:33 -05:00
Jarrod Johnson 86783a2f12 Fix uninitialized privacy_protocol variable 2026-02-03 07:58:07 -05:00
Jarrod Johnson 29ec8e2b54 Merge branch 'master' into async 2026-02-02 10:19:30 -05:00
Jarrod Johnson 99063eb049 Recognize variation in DeviceDescrption.json to see SMM3 2026-02-02 10:17:32 -05:00
Jarrod Johnson c83ac717fe Wire up license management async wise 2026-02-02 10:09:33 -05:00
Jarrod Johnson 69e149f9c5 Adjust ipmi get_licenses for async 2026-01-30 16:58:56 -05:00
Jarrod Johnson b496f2c324 Wire up a number of async style calls 2026-01-29 15:18:46 -05:00
Jarrod Johnson e75a1dc7ad Gracefully accept loop cancellation in async 2026-01-28 16:22:53 -05:00
Jarrod Johnson 5c6fb7f7ef Bring to current asyncio run best practices 2026-01-28 15:42:00 -05:00
Jarrod Johnson 2dcbf76738 More async rework of ipmi 2026-01-28 15:39:00 -05:00
Jarrod Johnson 04d2a5affc More async conversions 2026-01-28 15:32:54 -05:00
Jarrod Johnson 134f339050 Update some ipmi code for async 2026-01-28 15:06:53 -05:00
Jarrod Johnson b4b9a1d1ce Merge branch 'master' into async 2026-01-28 15:05:14 -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 291363c582 Update some code for async 2026-01-28 15:04:32 -05:00
Jarrod Johnson 3058dd4141 Update some code for async 2026-01-28 14:49:58 -05:00
Jarrod Johnson c29494bcf6 Make all the redfish iterators async consistent 2026-01-23 20:48:16 -05:00
Jarrod Johnson 50ec0bbca6 Correct to async for in refish retriev 2026-01-23 20:46:57 -05:00
Jarrod Johnson 52bb240aff Wire up async mechanism in redfish 2026-01-23 20:45:44 -05:00
Jarrod Johnson 667e44983d Fix ordering of confluentbmcname setting 2026-01-23 20:38:29 -05:00
Jarrod Johnson b5771023c3 Fix confetty indentation 2026-01-23 20:33:00 -05:00
Jarrod Johnson 76efea7c44 Use new method of running async in nodeattrib 2026-01-23 20:32:21 -05:00
Jarrod Johnson b0647275df Replace dead references to SecureHTTPConnection 2026-01-23 20:23:23 -05:00
Jarrod Johnson c4616745c4 Remove pyghmi usage across multiple areas 2026-01-23 13:31:09 -05:00
Jarrod Johnson 60c3d5400a Fix up proxmox module for async operation 2026-01-23 10:30:01 -05:00
Jarrod Johnson 6bc9282698 Change to await login 2026-01-22 14:59:31 -05:00
Jarrod Johnson b06ffb293a Asyncify proxmox retrieve function 2026-01-22 14:53:52 -05:00
Jarrod Johnson b548002a8d Fix nodegroup attribute async behavior 2026-01-22 14:50:59 -05:00
Jarrod Johnson 218ecce63f Correct import name 2026-01-22 14:46:34 -05:00
Jarrod Johnson 1c679727ad Correct issues in recent revision 2026-01-22 14:44:36 -05:00
Jarrod Johnson 50e530ebde Replace pyghmi with aiohmi in various plugins, remove some eventlet usage 2026-01-22 14:40:44 -05:00
Jarrod Johnson 7984c02042 Temporarily remove eficompressor dependency 2026-01-22 09:42:05 -05:00
Jarrod Johnson d338f8d586 Temporarily lift some rpm dependencies to work through dev 2026-01-22 09:26:45 -05:00
Jarrod Johnson c0d53ba986 Clean up RPM dependencies for async branch 2026-01-22 09:26:01 -05:00
Jarrod Johnson 68097428a5 Modernize asyncio invocation in main confluent runtime 2026-01-21 16:47:17 -05:00
Jarrod Johnson 7fedbc1810 Replace some pyghmi references and modernize some asyncio invocations 2026-01-21 16:45:42 -05:00
Jarrod Johnson b2f1b8da79 Add tasks management module for async 2026-01-21 16:23:31 -05:00
Jarrod Johnson 21c9158491 Carry forward some dns attributes into a bond 2026-01-21 15:12:23 -05:00
Jarrod Johnson 54735e9857 Carry forward some dns attributes into a bond 2026-01-21 15:11:45 -05:00
Jarrod Johnson 0dabccaec8 Corrections after some mistakes in the merge 2026-01-20 14:55:06 -05:00
Jarrod Johnson d89305ca42 Merge branch 'master' into async
Try to merge in 2025 work into async
2026-01-20 14:24:01 -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
Jarrod Johnson d66df7ee4b Merge branch 'master' into async
Need to rework httpapi further for changes to the firmware staging.
2025-01-08 14:23:52 -05:00
Jarrod Johnson feaa3bb7b4 Rework vinzmanager for async operation 2024-09-10 11:26:18 -04:00
Jarrod Johnson dac383af59 Merge branch 'master' into async 2024-09-10 09:51:28 -04:00
Jarrod Johnson f50db78c8c Merge branch 'master' into async 2024-08-31 07:30:47 -04:00
Jarrod Johnson 8cb34b20bc Fix duplicate lines from merge 2024-08-28 19:21:34 -04:00
Jarrod Johnson 75ae623b70 Merge branch 'master' into async 2024-08-28 19:20:21 -04:00
Jarrod Johnson 55cdfae437 Fix different invocations of check_fish
Particularly nodediscover register can fail.

Those invocations are XCC specific, so the targtype should not matter
in those cases.
2024-08-28 19:18:43 -04:00
Jarrod Johnson 4edc2a6412 Port forward Confluent 3.11 changes 2024-08-28 11:48:12 -04:00
Jarrod Johnson b46aecbeed Fix PXE afterm merge and have rebase work with async 2024-08-25 18:40:26 -04:00
Jarrod Johnson 69afc013f2 Merge branch 'master' into async 2024-08-23 18:26:33 -04:00
Jarrod Johnson 9c3126e9f7 Add client to asyncio pxe 2024-08-23 16:17:50 -04:00
Jarrod Johnson 0b401e8276 Merge branch 'master' into async 2024-08-23 15:51:04 -04:00
Jarrod Johnson b609a0039f Merge branch 'master' into async 2024-08-22 10:34:42 -04:00
Jarrod Johnson fe0a15faf2 Merge branch 'master' into async 2024-08-22 08:42:37 -04:00
Jarrod Johnson 10faac8835 Hook up descriptions to asyncio 2024-08-22 08:40:34 -04:00
Jarrod Johnson 19439463b1 Normalize non-http and http and http async and internal passthrough
Have the core provide normalization and use it across
places that need it.
2024-08-21 14:40:58 -04:00
Jarrod Johnson 1d861e60bb Refactor task management to its own module 2024-08-21 11:38:46 -04:00
Jarrod Johnson 52f172ef57 Merge branch 'master' into async 2024-08-21 09:56:43 -04:00
Jarrod Johnson a0ab71f7bb Fix call to check_fish with wrong number of args 2024-08-20 17:09:39 -04:00
Jarrod Johnson 4ef24351aa Do not await synchronous functions 2024-08-20 17:05:32 -04:00
Jarrod Johnson c9e428bb1b Change browserfs control to async, bring together to single send 2024-08-20 16:24:18 -04:00
Jarrod Johnson 53d0d09ae1 Have browserfs based import work with async 2024-08-20 15:57:56 -04:00
Jarrod Johnson 30b8979e2c Merge branch 'master' into async 2024-08-19 16:55:04 -04:00
Jarrod Johnson 6f776a657c Begin work on selfservice asyncio port
Have a deploycfg call be able to proceed through.
2024-08-16 17:06:49 -04:00
Jarrod Johnson 2f415caead Fix osdeploy updateboot with asyncio 2024-08-16 17:06:16 -04:00
Jarrod Johnson 708170b06a Convert affluent method from eventlet 2024-08-16 15:17:06 -04:00
Jarrod Johnson 5eaf998391 Remove greenlet, and change 'confluent' to asyncio 2024-08-16 14:36:59 -04:00
Jarrod Johnson c43a667299 Remove some debug output 2024-08-16 14:30:50 -04:00
Jarrod Johnson bf56d40fb5 Convert neighutil to asyncio 2024-08-16 14:29:04 -04:00
Jarrod Johnson fab6a5a757 Remove eventlet from log 2024-08-16 14:04:12 -04:00
Jarrod Johnson a076472718 Merge branch 'master' into async 2024-08-16 11:27:14 -04:00
Jarrod Johnson d1659cef97 Merge branch 'master' into async 2024-08-16 09:33:27 -04:00
Jarrod Johnson c0018840c7 Remove stale eventlet import from osimage 2024-08-15 16:40:40 -04:00
Jarrod Johnson 511fdfe6c1 Fix issues in the online debugger 2024-08-15 16:35:55 -04:00
Jarrod Johnson 4945c1f473 Replace 'backdoor' with 'debugger' 2024-08-15 16:03:02 -04:00
Jarrod Johnson 90b893bc28 Bring up ssh asyncio and fix other shell/console async 2024-08-15 14:45:31 -04:00
Jarrod Johnson ac4092ec4b More fixes for asyncio support console usage 2024-08-15 11:38:14 -04:00
Jarrod Johnson 556e40787c Have OpenBMC work with async changes 2024-08-15 11:28:08 -04:00
Jarrod Johnson 45187e0c54 Merge branch 'master' into async 2024-08-15 10:55:11 -04:00
Jarrod Johnson 2cc61a1810 Merge branch 'master' into async 2024-08-14 16:26:55 -04:00
Jarrod Johnson 21f68bb212 Apply formatting changes 2024-08-13 15:29:15 -04:00
Jarrod Johnson 45b17ba855 Get basic redfish running in asyncio 2024-08-13 15:28:52 -04:00
Jarrod Johnson db670b695f Reuse recent_peers
To be consistent, reuse this set rather than creating a new one.
2024-08-09 16:43:27 -04:00
Jarrod Johnson 0d8173cbcb Fix nodediscover clear
Nested async iteration of multiple confluent calls fail, break
it into two distinct queries.
2024-08-09 10:03:15 -04:00
Jarrod Johnson 8b70213c0d Merge branch 'master' into async 2024-08-09 07:56:30 -04:00
Jarrod Johnson e0fa642496 Fix SLP asyncio performance issue
SLP asyncio performance spent too much time tied up in futile
processing,
avoid duplicate deferpeers and simplify the loop iteration.
2024-08-08 17:06:57 -04:00
Jarrod Johnson 42e5a556c1 Make it easier to debug slow callback
Provide a name to create_task to make the
slow callback warning actually usable.
2024-08-08 16:25:04 -04:00
Jarrod Johnson d7d89dd233 Remove spurious import from redfish 2024-08-08 16:07:42 -04:00
Jarrod Johnson 536aa7f212 Fix XCC/XCC2 discovery for async branch 2024-08-08 14:25:47 -04:00
Jarrod Johnson 67a61c5012 Migrate XCC3 discovery to async 2024-08-08 12:45:39 -04:00
Jarrod Johnson 73cd6d52da Remove spurious reintroduction of select to slp 2024-08-08 11:05:51 -04:00
Jarrod Johnson 5be422958b Removed redundant definitions introduced by merge attempt 2024-08-08 10:10:16 -04:00
Jarrod Johnson c754dc2641 Merge branch 'master' into async 2024-08-08 09:45:15 -04:00
Jarrod Johnson 3741db740f Merge branch 'master' into async 2024-07-23 16:20:14 -04:00
Jarrod Johnson e446aa9277 Merge branch 'master' into async 2024-07-09 08:45:43 -04:00
Jarrod Johnson 1edfeba076 Add MegaRAC discovery support for recent MegaRAC
Create a generic redfish discovery and a MegaRAC specific
variant.

This should open the door for more generic common base redfish discovery
for vaguely compatible implementations.  For now, MegaRAC only
overrides the default username and password (which is undefined
in the redfish spec).

Also, have SSDP recognize the variant, and tolerate odd nonsense
like SSDP replies coming from all manner of odd port numbers (no
way to make a sane firewall rule to capture that odd behavior,
but at application level we have a chance).
2024-07-03 14:36:28 -04:00
Jarrod Johnson 2c2fe08d66 Merge branch 'megaracdisco' into async 2024-07-02 15:13:33 -04:00
Jarrod Johnson 362f6ae6d5 Merge branch 'master' into async 2024-07-02 15:13:26 -04:00
Jarrod Johnson 8fbb495ee9 Merge branch 'master' into async 2024-06-24 15:57:55 -04:00
Jarrod Johnson 879fb9c7ab Merge branch 'master' into async 2024-06-14 11:22:05 -04:00
Jarrod Johnson 9b8ec1e493 Merge branch 'master' into async 2024-06-14 11:16:34 -04:00
Jarrod Johnson 9394e83c81 Avoid pam blocking main thread execution
Use processpool to execute pam authentication,
avoiding a hang while waiting for child process.
2024-06-14 10:47:02 -04:00
Jarrod Johnson f42812b836 Fix console over shared websocket
This fixes console behavior in the webui
2024-06-13 16:57:20 -04:00
Jarrod Johnson b6a0250e5c Advance state of asyncio
Add a mechanism to close a session the right way
in tlvdata

Fix confluentdbutil/configmanager to restore/dump db to directory

Move auth to asyncio away from eventlet

Fix some issues with httpapi, enable reading body via aiohttp

Fix health from ipmi plugin

Fix user creation across a collective.
2024-06-13 16:32:02 -04:00
Jarrod Johnson bdb7f064d6 Rework a number of subprecess calls and osdeploy
Some subprocess calls were reworked to use asyncio friendly
variants.

Also, osdeploy initialize was checked, and reworked the ssh and tls
handling.

osdeploy import was also reworked to functional with async only.
2024-05-31 17:22:26 -04:00
Jarrod Johnson 85c8268ad8 Fix proxy console through collective in async 2024-05-30 16:14:39 -04:00
Jarrod Johnson 00eff4a002 Migrate IPMI SOL to asyncio 2024-05-30 15:37:06 -04:00
Jarrod Johnson cbb52739d3 Fix a number of issues with async rework
Have util retain tasks that are 'fire and forget', to avoid
garbage collection trying to delete the background tasks.

Move some utilities explicitly over to asynclient/asynctlvdata that
had previously been reworked.

Implement terminal resize in new asyncssh backend.
2024-05-30 13:59:14 -04:00
Jarrod Johnson 4ba82b7ef4 Merge branch 'master' into async 2024-05-30 09:29:24 -04:00
Jarrod Johnson c5405f832c Advance state of async shellserver
Can successfully run ssh sessions through
confluent with async now
2024-05-29 20:18:07 -04:00
Jarrod Johnson 23d0bbd047 Move nodediscover to async client
The work to convert had already been done, and it may be handy to make
nodediscover do some async tricks in the future.
2024-05-29 12:24:19 -04:00
Jarrod Johnson 4c3f93765f Have async and traditional client
Since a lot of the traditional client did not need async,
make life easier by just having them in parallel for now.

The server must use the async client, but the client applications can
stick with the somewhat more straightforward synchronous client.
2024-05-29 12:23:05 -04:00
Jarrod Johnson 4a2349d9ad Merge branch 'master' into async 2024-05-23 15:15:59 -04:00
Jarrod Johnson 1a9395fc5f Amend EL network bringup
One issue is that there are multiple networkmanager connections,
clean this up, though this seems not to be a functional issue.

However, sometimes the lldpad usage screws up network configuration,
disable the facility by forcibly disabling fcoe sincec that is what triggers lldpad.
wq
2024-05-22 15:44:05 -04:00
Jarrod Johnson b4ae6012c5 Remove eventlet from PXE support 2024-05-20 16:27:11 -04:00
Jarrod Johnson 782991aea3 Switch to asyncio usage of pysnmnp
This requires pysnm 6, the edition that should become the official one,
maintained by lextudio
2024-05-20 11:48:53 -04:00
Jarrod Johnson 6e751c811e Begin rework of macmap.py
Redo offload to asyncio subprocess, and
replace eventlet Events with futures for
messaging.
2024-05-17 17:07:18 -04:00
Jarrod Johnson c03aa728cc Properly detect killed leader
If leader closes connection, then have get_next_msg return None
as it did before.
2024-05-17 16:03:37 -04:00
Jarrod Johnson fbdb35e33d Merge branch 'master' into async 2024-05-16 15:42:22 -04:00
Jarrod Johnson 207cc3471e Fix closing sockets in various contexts
With asyncio, we must close the writer half of a pair

Also rework the get_next_msg to work better.

Still need to allow stop_following to interrupt get_next_msg
2024-05-16 15:40:43 -04:00
Jarrod Johnson 5a9f608451 Fix handling some eatonpdu return values 2024-05-15 12:30:13 -04:00
Jarrod Johnson 100810788c Fix media location search for EL8
EL8 distributions marked the 'OS' as dracut, workaround by trying to use PRETTY_NAME
2024-05-15 12:28:41 -04:00
Jarrod Johnson 90b90ade9c Remove disused iovec
iovec is no longer used due to migration from relevant
recvmsg ctypes call.
2024-05-09 09:49:56 -04:00
Jarrod Johnson f6fc539df9 Remove disused recvmsg ctypes wrapper
Since going to builtin python recvmsg, remove
the ctypes wrapper.
2024-05-09 09:48:11 -04:00
Jarrod Johnson 2e30f7fb86 Prune unneeded ctypes material from pxe
Moving to .recvmsg from python socket eliminates
most of the ctypes requirement. Still using it for sendto.
2024-05-09 09:46:01 -04:00
Jarrod Johnson e1e3244af6 Port PXE to asyncio and re-enable 2024-05-09 09:40:03 -04:00
Jarrod Johnson 5fd0cf2b0b Begin conversion of pxe to asyncio
Also convert to 'natural' recvmsg now that we are requiring
python high enough to have it.
2024-05-08 17:18:07 -04:00
Jarrod Johnson bd2f08d3ad Reactive SSDP in discovery core
Also, fix a getaddrinfo call to be async.
2024-05-08 13:18:38 -04:00
Jarrod Johnson b9a2c9a3ae Convert more XCC handling to asyncio 2024-05-08 13:18:05 -04:00
Jarrod Johnson 42b7cbe421 Implement SSDP asyncio
This covers SSDP devices as well as confluent deployment
discovery.
2024-05-08 13:17:43 -04:00
Jarrod Johnson 96a43013b5 Merge branch 'master' into async 2024-05-08 11:51:16 -04:00
Jarrod Johnson a3506cf0bf Correct misrouting in slp
IPv4 scan responses were lost as
the reader was passed IPv6 socket
no matter what.

Also, remove some needless verbosity.
2024-05-08 11:48:46 -04:00
Jarrod Johnson 25d4d13a96 Finish conversion of slp to asyncio.
Make process_peer async, with socket connection being async,
and dependency.

Have getaddrinfo use the asyncio version.

Rework the snoop to be more effective.

Rework the scan to be less convoluted.
2024-05-08 11:35:33 -04:00
Jarrod Johnson 23658680a5 Have slp mostly work
Advance the SLP discovery code and core discovery
to mostly work.
2024-05-07 17:02:51 -04:00
Jarrod Johnson 2089f5e7e6 Deal with normal generator from a plugin 2024-05-07 17:01:04 -04:00
Jarrod Johnson b3e0117944 Fix getpeername invocation in async 2024-05-07 17:00:43 -04:00
Jarrod Johnson 056a41c985 Fix client async invocations 2024-05-07 17:00:25 -04:00
Jarrod Johnson 6704f23218 Merge branch 'master' into async 2024-05-07 10:07:08 -04:00
Jarrod Johnson 222bdee851 Load firewall before esxi installation begins
Parts of esxi install depend on firewall running.  When
we are done with 'odd' networking, restore firewall
to meet that expectation.
2024-05-07 10:05:50 -04:00
Jarrod Johnson 5e222041bf Merge branch 'master' into async 2024-05-03 10:27:31 -04:00
Jarrod Johnson ee6f869cea Port utilities to asyncio, selfcheck and osdeploy
confluent_selfcheck removes eventlet dependency,

osdeploy reworked to use async methods to work with new client.
2024-04-30 14:30:01 -04:00
Jarrod Johnson b967c552fd Migrate intra-collective requests to asyncio
Update dispatch to be asyncio based, remove eventlet from core

Clean up some overly verbose print statements.
2024-04-30 13:56:00 -04:00
Jarrod Johnson 553916340e Advanced asyncio port progress
Offer a function in core to normalize plugin return.

A plugin might return an async generator, a traditional generator,
or might even return an awaitable wrapping a traditional generator.

Replace eventlet spawn with util spawn in discover core

Have node attribute update await the set_node_attributes appropriately
2024-04-30 10:44:43 -04:00
Jarrod Johnson 0be60b1ce2 Merge branch 'master' into async 2024-04-29 10:55:58 -04:00
Jarrod Johnson a5dc10debf Fix attribute synchronization
Specify a finite read to actually return from the buffer.

Convert some functions to async/await as appropriate.
2024-04-29 10:54:30 -04:00
Jarrod Johnson d2edcb62c6 Begin implementation of asyncio collective
The config synchronization is in progress.
2024-04-26 15:48:14 -04:00
Jarrod Johnson afa0c0df5a Merge branch 'master' into async 2024-04-22 14:36:42 -04:00
Jarrod Johnson 560ec60c12 Merge branch 'master' into async 2024-04-17 15:18:58 -04:00
Jarrod Johnson e890276bf6 Advance state of collective in asyncio
Eventlet is nominally removed from collective manager, however the join process still
needs to be reworked, and a lot more flows need to be adjusted.
2024-04-16 16:53:45 -04:00
Jarrod Johnson c24da59216 Merge branch 'master' into async 2024-04-16 10:39:20 -04:00
Jarrod Johnson e8110551db Port some of the collective management to asyncio 2024-04-15 17:19:27 -04:00
Jarrod Johnson bfe7529d21 Merge branch 'master' into async 2024-04-15 10:04:19 -04:00
Jarrod Johnson c3cafd9bf8 Purge eventlet and greenlet and long-polling support
Rather than try to support long deprecated http api behavior,
purge it for simpler code and remove eventlet/greenlet from the http
stack.
2024-04-11 09:09:02 -04:00
Jarrod Johnson fb8ac158cb Merge branch 'master' into async 2024-04-11 08:14:20 -04:00
Jarrod Johnson 9d828f0998 Merge branch 'master' into async 2024-04-09 13:37:09 -04:00
Jarrod Johnson e8fed28a21 Do not disarm until client notify done
In the unlikely event of a hiccup during the credserver connection,
defer the disarm until the server has transmitted success.
2024-04-09 10:28:21 -04:00
Jarrod Johnson 7b2e32009f Numerous async improvements
Restore 'as available' behavior to noderange over socket

Bring the httpapi to the point where the webui is able to start working,
notably bringing the asynchttp online with the websocket.

Fix a flaw in the async ipmi that would cause hangups.
2024-04-04 17:13:37 -04:00
Jarrod Johnson 587ccd13cc More work toward asyncio
aiohttp now covers a lot of httpapi GET, and some of websocket.
2024-04-03 16:58:40 -04:00
Jarrod Johnson 198ffb8be6 Advance asyncio port
Purge sockapi of remaining eventlet call

Extend asyncio into the credserver to finish out sockapi.

Have client and sockapi complete TLS connection including password checking

Fix confetty ability to 'create'.
2024-04-01 16:38:10 -04:00
Jarrod Johnson c1d680d8d8 Merge branch 'master' into async 2024-04-01 12:15:47 -04:00
Jarrod Johnson 1fbaee6149 Further move toward asyncio and reduce PyOpenSSL dep
Since we are rebasing to at least Python 3.6, and with
some extra ctypes wranging of the ssl context, we can likely
remove PyOpenSSL. Take first steps by removing it from 'sockapi'.

Have confluent executable become the 'top level' for eventlet, to allow
work on 'de-eventleting' on 'main.py'.

Rework tlvdata to deal with either a socket or a reader, writer tuple.
Using TLS with asyncio is easiest with the 'open_connection'
semantics, which force either a Protocol handler (callback based) or
dual streams.  While protocol approach ends with a more socket-like
'transport', the 'protocol' half is a bit unwieldy. So reader and writer
streams instead.
2024-03-29 16:23:45 -04:00
Jarrod Johnson 81428727d3 Merge branch 'master' into async 2024-03-27 14:28:48 -04:00
Jarrod Johnson 668c5af261 Bugfix and rework consoleserver a bit
Fix incorrect syntax in ssh.py, and correct direct asyncio
call of sock_recv when it must be called on the loop.
2024-03-25 15:18:05 -04:00
Jarrod Johnson b1cd7bcd98 Wire up 'configuration/system/all' in async way
This allows the fundamental API call to pass through
2024-03-25 15:16:56 -04:00
Jarrod Johnson 4fe9e1e80b Merge branch 'master' into async 2024-03-25 08:07:33 -04:00
Jarrod Johnson 508adc8d03 Merge master into asyncio 2024-03-22 15:52:04 -04:00
Jarrod Johnson 46edd8a49a Add a stub backdoor replacement 2024-03-15 17:11:12 -04:00
Jarrod Johnson 0570996c36 Merge branch 'master' into async 2024-03-15 15:51:08 -04:00
Jarrod Johnson ce3d4d7256 Merge branch 'master' into async 2024-03-15 13:04:01 -04:00
Jarrod Johnson 94cb1aebc3 Work on nodeconfig async conversion
Refactor nodeconfig to stand a chance at async.
2024-03-15 12:50:46 -04:00
Jarrod Johnson da63543a70 Advance the state of asyncio port 2024-03-15 12:50:04 -04:00
Jarrod Johnson 887207b9fc Merge branch 'master' into async 2024-03-15 12:31:51 -04:00
Jarrod Johnson 142f97c94e Merge branch 'master' into async 2024-03-15 09:58:06 -04:00
Jarrod Johnson 4ca82948ba SSH test by IP, to reflect actual usage and catch issues
One issue is modified ssh_known_hosts wildcard customization
failing to cover IP address.
2024-03-14 11:20:36 -04:00
Jarrod Johnson 399c1467c1 Remove redundant kill on the agent pid
Extraneous kill on the agent pid is removed.
2024-03-14 10:53:13 -04:00
Jarrod Johnson dcb6a1c759 Updates to confluent_selfcheck
Reap ssh-agent to avoid stale agents lying around.

Remove nuisance warnings about virbr0 when present.

Do a full runthrough as the confluent user to ssh to a node when user
requests with '-a', marking known_hosts and automation key issues.
2024-03-14 10:50:01 -04:00
Jarrod Johnson 91dc37d45e Fix nodeapply redoing a single node multiple times 2024-03-12 15:32:44 -04:00
Jarrod Johnson 500a955d79 Fix confetty tab completion with async
async required the async client to be wrapped in sync code.
2024-03-12 13:13:19 -04:00
Jarrod Johnson f4f5fcdb6d Fix lldp when peername is null
Some neighbors result in a null name, handle that.
2024-03-12 09:36:40 -04:00
Jarrod Johnson eed2e74bd0 Have image2disk delay exit on error
Debugging cloning is difficult when system immediately reboots on error.
2024-03-11 17:10:33 -04:00
Jarrod Johnson 4f92e3413a Expose fingerprinting and better error handling to osdeploy
This allows custom name and pre-import checking.
2024-03-11 13:32:45 -04:00
Jarrod Johnson d42e8e0921 Further asyncio port of confluent
Advance state of basic clients to advance testing and soon start doing
deeper activity.
2024-03-06 16:50:34 -05:00
Jarrod Johnson 635ef6073c Fix stray blank line at end of nodelist
Wrong indentation level for nodelist resulting in
spurious line.
2024-03-06 16:28:09 -05:00
Jarrod Johnson 496e7b4ef3 Properly address runansible error relay 2024-03-06 09:27:53 -05:00
Jarrod Johnson 3d33e33ea2 Dump stderr to client if ansible had an utterly disastrous condition 2024-03-06 08:45:23 -05:00
Jarrod Johnson 0a8ec96cdf Further progress toward asyncio
Basic operations can now happen with some async flows.
2024-03-04 16:18:55 -05:00
Jarrod Johnson 25f2698ae6 Opportunisticlly use sshd_config.d when detected 2024-03-04 08:06:01 -05:00
Jarrod Johnson d6bff637db Commence work on async 2024-02-23 11:56:07 -05:00
Jarrod Johnson 91e0aa938c Remove disused bufferlock
We no longer use a lock on buffer communication, eliminate
the stale variable.
2024-02-22 15:07:12 -05:00
Jarrod Johnson ed54bfa11a Change to unix domain for vtbuffer communication
The semaphore arbitrated single channel sharing
was proving to be too slow.  Make the communication
lockless by having dedicated sockets per request.
2024-02-22 15:05:56 -05:00
149 changed files with 9911 additions and 6076 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/
+32 -15
View File
@@ -131,6 +131,15 @@ def print_help():
#common with the api document
def writeout(data):
while True:
try:
select.select((), (sys.stdout,), ())
sys.stdout.write(data)
break
except BlockingIOError:
continue
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
status = consolename
@@ -150,10 +159,10 @@ def updatestatus(stateinfo={}):
if 'state' in stateinfo: # currently only read power means anything
newpowerstate = stateinfo['state']['value']
if newpowerstate != powerstate and newpowerstate == 'off':
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
writeout("\x1b[2J\x1b[;H[powered off]\r\n")
clearpowermessage = True
if newpowerstate == 'on' and clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
writeout("\x1b[2J\x1b[;H")
clearpowermessage = False
powerstate = newpowerstate
if 'clientcount' in laststate and laststate['clientcount'] != 1:
@@ -171,7 +180,7 @@ def updatestatus(stateinfo={}):
if info:
status += ' [' + ','.join(info) + ']'
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;console: %s\x07' % status)
writeout('\x1b]0;console: %s\x07' % status)
sys.stdout.flush()
@@ -451,7 +460,7 @@ def do_command(command, server):
print_result(res)
elif argv[0] == 'start':
targpath = fullpath_target(argv[1])
nodename = targpath.split('/')[-3]
nodename = targpath.split('/')[2]
currconsole = targpath
startrequest = {'operation': 'start', 'path': targpath,
'parameters': {}}
@@ -659,9 +668,10 @@ def get_session_node(shellargs):
return targ, shellargs[0]
if len(shellargs) == 2 and shellargs[0] == 'start':
args = [s for s in shellargs[1].split('/') if s]
if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \
args[3] == 'session':
return shellargs[1], args[1]
if len(args) == 4 and args[0] == 'nodes':
if args[2] == 'console' and \
args[3] == 'session':
return shellargs[1], args[1]
if len(args) == 5 and args[0] == 'nodes' and args[2] == 'shell' and \
args[3] == 'sessions':
return shellargs[1], args[1]
@@ -1045,14 +1055,21 @@ def consume_termdata(fh, bufferonly=False):
clearpowermessage = False
if bufferonly:
return data
try:
sys.stdout.write(data)
except UnicodeEncodeError:
sys.stdout.buffer.write(data.encode('utf8'))
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
data = data.encode('utf8')
written = False
while not written:
select.select((), (sys.stdout,), ())
try:
sys.stdout.buffer.write(data)
written = True
except BlockingIOError:
continue
except IOError: # Some times circumstances are bad
# resort to byte at a time...
raise
for d in data:
sys.stdout.write(d)
written = True
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
+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]:
+118 -113
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -17,6 +17,7 @@
__author__ = 'alin37'
import asyncio
from getpass import getpass
import optparse
import os
@@ -34,126 +35,130 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
\n %prog -c noderange <list of attributes> \
\n %prog -e noderange <attribute names to set> \
\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
argparser.add_option('-m', '--maxnodes', type='int',
help='Prompt if trying to set attributes on more '
'than specified number of nodes')
argparser.add_option('-s', '--set', dest='set', metavar='settings.batch',
default=False, help='set attributes using a batch file')
(options, args) = argparser.parse_args()
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
\n %prog -c noderange <list of attributes> \
\n %prog -e noderange <attribute names to set> \
\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
argparser.add_option('-m', '--maxnodes', type='int',
help='Prompt if trying to set attributes on more '
'than specified number of nodes')
argparser.add_option('-s', '--set', dest='set', metavar='settings.batch',
default=False, help='set attributes using a batch file')
(options, args) = argparser.parse_args()
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
#Sets attributes
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
try:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
requestargs=args[2:]
elif args[1] == 'current':
showtype = 'current'
requestargs=args[2:]
else:
showtype = 'all'
requestargs=args[1:]
except:
pass
elif options.clear or options.environment or options.prompt:
sys.stderr.write('Attribute names required with specified options\n')
argparser.print_help()
exitcode = 400
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
elif options.set:
arglist = [noderange]
showtype='current'
argfile = open(options.set, 'r')
argset = argfile.readline()
while argset:
#Sets attributes
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
await session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode = await client.updateattrib(session,args,nodetype, noderange, options, argassign)
try:
argset = argset[:argset.index('#')]
except ValueError:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
requestargs=args[2:]
elif args[1] == 'current':
showtype = 'current'
requestargs=args[2:]
else:
showtype = 'all'
requestargs=args[1:]
except:
pass
argset = argset.strip()
if argset:
arglist += shlex.split(argset)
elif options.clear or options.environment or options.prompt:
sys.stderr.write('Attribute names required with specified options\n')
argparser.print_help()
exitcode = 400
elif options.set:
arglist = [noderange]
showtype='current'
argfile = open(options.set, 'r')
argset = argfile.readline()
session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode=client.updateattrib(session,arglist,nodetype, noderange, options, None)
if exitcode != 0:
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
arglist += shlex.split(argset)
argset = argfile.readline()
session.stop_if_noderange_over(noderange, options.maxnodes)
exitcode=client.updateattrib(session,arglist,nodetype, noderange, options, None)
if exitcode != 0:
sys.exit(exitcode)
# Lists all attributes
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = await client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
# Lists all attributes
if __name__ == '__main__':
asyncio.run(main())
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
+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)
+111 -106
View File
@@ -15,7 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import os
import signal
import optparse
@@ -31,7 +31,7 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
class NullOpt(object):
blame = None
@@ -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, '
@@ -222,109 +222,114 @@ def parse_config_line(arguments, single=False):
queryparms[path] = {}
queryparms[path][attrib] = param
if options.batch:
printsys = []
argfile = open(options.batch, 'r')
argset = argfile.readline()
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
parse_config_line(shlex.split(argset), single=True)
async def main():
global printsys
if options.batch:
printsys = []
argfile = open(options.batch, 'r')
argset = argfile.readline()
else:
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault:
session.stop_if_noderange_over(noderange, options.maxnodes)
if options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
elif options.restoredefault.lower() in (
'bmc', 'imm', 'xcc'):
for fr in session.update(
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
parse_config_line(shlex.split(argset), single=True)
argset = argfile.readline()
else:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
session.stop_if_noderange_over(noderange, options.maxnodes)
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
if key.startswith('bmc.'):
path = 'configuration/management_controller/extended/all'
attrib = key.replace('bmc.', '')
else:
path = 'configuration/system/all'
attrib = key
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault:
await session.stop_if_noderange_over(noderange, options.maxnodes)
if options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
async for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
elif options.restoredefault.lower() in (
'bmc', 'imm', 'xcc'):
async for fr in session.update(
'/noderange/{0}/configuration/management_controller/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
if 'value' not in r:
continue
keyval = r['value']
key, val = keyval.split('=')
if key in attrnamebypath[path]:
key = attrnamebypath[path][key]
print('{0}: {1}: {2}'.format(node, key, val))
else:
for path in queryparms:
if options.comparedefault:
continue
rcode |= client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
if printsys == 'all' or printextbmc or printbmc or printallbmc:
if printbmc or not printextbmc:
rcode |= client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
session, printbmc, options, attrprefix='bmc.')
if options.extra:
if options.advanced:
rcode |= client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
session, printextbmc, options)
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
await session.stop_if_noderange_over(noderange, options.maxnodes)
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
if key.startswith('bmc.'):
path = 'configuration/management_controller/extended/all'
attrib = key.replace('bmc.', '')
else:
path = 'configuration/system/all'
attrib = key
else:
rcode |= client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
session, printextbmc, options)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode = client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
async for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
if 'value' not in r:
continue
keyval = r['value']
key, val = keyval.split('=')
if key in attrnamebypath[path]:
key = attrnamebypath[path][key]
print('{0}: {1}: {2}'.format(node, key, val))
else:
for path in queryparms:
if options.comparedefault:
continue
rcode |= await client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
if printsys == 'all' or printextbmc or printbmc or printallbmc:
if printbmc or not printextbmc:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
session, printbmc, options, attrprefix='bmc.')
if options.extra:
if options.advanced:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra_advanced'.format(noderange),
session, printextbmc, options)
else:
rcode |= await client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
session, printextbmc, options)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode = await client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
if __name__ == '__main__':
asyncio.run(main())
+6 -2
View File
@@ -134,7 +134,9 @@ def determine_tile_size(numnodes):
# from kitty by omitting, but:
# then we don't know how much to move the cursor left after draw_image
# Konsole won't scale at all with only partial scaling specified
cheight, cwidth, pixwidth, pixheight = sq.get_screengeom()
direct_console()
cheight, cwidth, pixwidth, pixheight = sq.get_screengeom(escfallback=True)
indirect_console()
# 16:12 is to roughly account for the 'titles' of the tiles
ratio = (pixwidth / 16) / (pixheight / 12)
bestdeviation = None
@@ -214,7 +216,9 @@ def cursor_show():
sys.stdout.write('\x1b[?25h')
def get_pix_dimensions(width, height):
cheight, cwidth, pixwidth, pixheight = sq.get_screengeom()
direct_console()
cheight, cwidth, pixwidth, pixheight = sq.get_screengeom(escfallback=True)
indirect_console()
imgwidth = int(pixwidth / cwidth * width)
imgheight = int(pixheight / cheight * height)
return imgwidth, imgheight
+31 -26
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import optparse
import os
import signal
@@ -30,29 +31,33 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs = None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
async for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.run(main())
+41 -41
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import csv
import optparse
import os
@@ -26,7 +27,7 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
import confluent.sortutil as sortutil
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
@@ -81,16 +82,15 @@ def subscribe_discovery(options, session, subscribe, targ):
if 'status' in rsp:
print(rsp['status'])
def print_disco(options, session, currmac, outhandler, columns):
async def print_disco(options, session, currmac, outhandler, columns):
procinfo = {}
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
async for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
if 'Switch' in columns or 'Port' in columns:
if 'switch' in procinfo:
procinfo['port'] = procinfo['switchport']
else:
for tmpinfo in session.read(
async for tmpinfo in session.read(
'/networking/macs/by-mac/{0}'.format(currmac)):
if 'ports' in tmpinfo:
# The api sorts so that the most specific available value
@@ -160,12 +160,12 @@ def datum_complete(datum):
searchkeys = set(['mac', 'serial', 'uuid'])
def search_record(datum, options, session):
async def search_record(datum, options, session):
for searchkey in searchkeys:
options.__dict__[searchkey] = None
for searchkey in searchkeys & set(datum):
options.__dict__[searchkey] = datum[searchkey]
return list(list_matching_macs(options, session))
return [x async for x in list_matching_macs(options, session)]
def datum_to_attrib(datum):
@@ -180,7 +180,7 @@ def datum_to_attrib(datum):
unique_fields = frozenset(['serial', 'mac', 'uuid'])
def import_csv(options, session):
async def import_csv(options, session):
nodedata = []
unique_data = {}
exitcode = 0
@@ -213,7 +213,7 @@ def import_csv(options, session):
for nodedatum in alldata:
if not search_record(nodedatum, options, session) and not broken:
allthere = False
blocking_scan(session)
await blocking_scan(session)
break
for nodedatum in alldata:
if not allthere and not search_record(nodedatum, options, session):
@@ -262,7 +262,7 @@ def import_csv(options, session):
sys.exit(exitcode)
def list_discovery(options, session):
async def list_discovery(options, session):
orderby = None
if options.fields:
columns = []
@@ -278,23 +278,24 @@ def list_discovery(options, session):
if options.order.lower() == field.lower():
orderby = field
outhandler = client.Tabulator(columns)
for mac in list_matching_macs(options, session):
print_disco(options, session, mac, outhandler, columns)
for mac in [x async for x in list_matching_macs(options, session)]:
await print_disco(options, session, mac, outhandler, columns)
if options.csv:
outhandler.write_csv(sys.stdout, orderby)
else:
for row in outhandler.get_table(orderby):
print(row)
def clear_discovery(options, session):
for mac in list_matching_macs(options, session):
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
async def clear_discovery(options, session):
allmacs = [x async for x in list_matching_macs(options, session)]
for mac in allmacs:
async for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
if 'deleted' in res:
print('Cleared info for {0}'.format(res['deleted']))
else:
print(repr(res))
def list_matching_macs(options, session, node=None, checknode=True):
async def list_matching_macs(options, session, node=None, checknode=True):
path = '/discovery/'
if node:
path += 'by-node/{0}/'.format(node)
@@ -314,22 +315,21 @@ def list_matching_macs(options, session, node=None, checknode=True):
path += 'by-state/{0}/'.format(options.state).lower()
if options.mac:
path += 'by-mac/{0}'.format(options.mac)
result = list(session.read(path))[0]
result = list([x async for x in session.read(path)])[0]
if 'error' in result:
return []
return [options.mac.replace(':', '-')]
return
yield options.mac.replace(':', '-')
return
else:
path += 'by-mac/'
ret = []
for x in session.read(path):
async for x in session.read(path):
if 'item' in x and 'href' in x['item']:
ret.append(x['item']['href'])
return ret
yield x['item']['href']
def assign_discovery(options, session, needid=True):
async def assign_discovery(options, session, needid=True):
abort = False
if options.importfile:
return import_csv(options, session)
return await import_csv(options, session)
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
@@ -340,16 +340,16 @@ def assign_discovery(options, session, needid=True):
abort = True
if abort:
sys.exit(1)
matches = list_matching_macs(options, session, None if needid else options.node, False)
matches = [x async for x in list_matching_macs(options, session, None if needid else options.node, False)]
if not matches:
# Do a rescan to catch missing requested data
blocking_scan(session)
matches = list_matching_macs(options, session, None if needid else options.node, False)
await blocking_scan(session)
matches = [x async for x in list_matching_macs(options, session, None if needid else options.node, False)]
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
exitcode = 0
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
async for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
{'node': options.node}):
if 'assigned' in res:
print('Assigned: {0}'.format(res['assigned']))
@@ -361,14 +361,14 @@ def assign_discovery(options, session, needid=True):
if exitcode:
sys.exit(exitcode)
def blocking_scan(session):
list(session.update('/discovery/rescan', {'rescan': 'start'}))
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
async def blocking_scan(session):
list([x async for x in session.update('/discovery/rescan', {'rescan': 'start'})])
while(list([x async for x in session.read('/discovery/rescan')])[0].get('scanning', False)):
time.sleep(0.5)
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
list([x async for x in session.update('/networking/macs/rescan', {'rescan': 'start'})])
def main():
async def main():
parser = optparse.OptionParser(
usage='Usage: %prog [list|assign|rescan|clear|subscribe|unsubscribe|register] [options]')
# -a for 'address' maybe?
@@ -419,13 +419,13 @@ def main():
sys.exit(1)
session = client.Command()
if args[0] == 'list':
list_discovery(options, session)
await list_discovery(options, session)
if args[0] == 'clear':
clear_discovery(options, session)
await clear_discovery(options, session)
if args[0] == 'assign':
assign_discovery(options, session)
await assign_discovery(options, session)
if args[0] == 'reassign':
assign_discovery(options, session, False)
await assign_discovery(options, session, False)
if args[0] == 'register':
register_endpoint(options, session, args[1])
if args[0] == 'subscribe':
@@ -433,9 +433,9 @@ def main():
if args[0] == 'unsubscribe':
subscribe_discovery(options, session, False, args[1])
if args[0] == 'rescan':
blocking_scan(session)
await blocking_scan(session)
print("Rescan complete")
if __name__ == '__main__':
main()
asyncio.run(main())
+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:
+7 -6
View File
@@ -1,4 +1,4 @@
#!/usr/libexec/platform-python
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -21,6 +21,7 @@ import optparse
import os
import signal
import sys
import asyncio
@@ -33,9 +34,9 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
def main():
async def main():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange\n"
" or: %prog [options] noderange <nodeattribute>...")
@@ -59,9 +60,9 @@ def main():
requestargs=args[1:]
nodetype='noderange'
if len(args) > 1:
exitcode=client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
exitcode=await client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
else:
for res in session.read(nodelist):
async for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
@@ -73,4 +74,4 @@ def main():
sys.exit(exitcode)
if __name__ == '__main__':
main()
asyncio.run(main())
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
+29 -22
View File
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import optparse
import os
import signal
@@ -32,26 +33,32 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog <noderange>
async def main():
argparser = optparse.OptionParser(
usage='''\n %prog <noderange>
\n ''')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to delete, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
session.stop_if_noderange_over(noderange, options.maxnodes)
for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to delete, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
await session.stop_if_noderange_over(noderange, options.maxnodes)
async for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
+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']]
+827
View File
@@ -0,0 +1,827 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# 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.
import asyncio
import ctypes
import ctypes.util
import dbm
import csv
import errno
import fnmatch
import hashlib
import os
import shlex
import socket
import ssl
import sys
import confluent.asynctlvdata as tlvdata
import confluent.sortutil as sortutil
libssl = ctypes.CDLL(ctypes.util.find_library('ssl'))
libssl.SSL_CTX_set_cert_verify_callback.argtypes = [
ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'switchuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
try:
getinput = raw_input
except NameError:
getinput = input
class PyObject_HEAD(ctypes.Structure):
_fields_ = [
("ob_refcnt", ctypes.c_ssize_t),
("ob_type", ctypes.c_void_p),
]
# see main/Modules/_ssl.c, only caring about the SSL_CTX pointer
class PySSLContext(ctypes.Structure):
_fields_ = [
("ob_base", PyObject_HEAD),
("ctx", ctypes.c_void_p),
]
@ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)
def verify_stub(store, misc):
return 1
class NestedDict(dict):
def __missing__(self, key):
value = self[key] = type(self)()
return value
def stringify(instr):
# Normalize unicode and bytes to 'str', correcting for
# current python version
if isinstance(instr, bytes) and not isinstance(instr, str):
return instr.decode('utf-8', errors='replace')
elif not isinstance(instr, bytes) and not isinstance(instr, str):
return instr.encode('utf-8')
return instr
class Tabulator(object):
def __init__(self, headers):
self.headers = headers
self.rows = []
def add_row(self, row):
self.rows.append(row)
def get_table(self, order=None):
i = 0
fmtstr = ''
separator = []
for head in self.headers:
if order and order == head:
order = i
neededlen = len(head)
for row in self.rows:
if len(row[i]) > neededlen:
neededlen = len(row[i])
separator.append('-' * (neededlen + 1))
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
i = i + 1
fmtstr = fmtstr[:-1]
yield fmtstr.format(*self.headers)
yield fmtstr.format(*separator)
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
yield fmtstr.format(*row)
else:
for row in self.rows:
yield fmtstr.format(*row)
def write_csv(self, output, order=None):
output = csv.writer(output)
output.writerow(self.headers)
i = 0
for head in self.headers:
if order and order == head:
order = i
i = i + 1
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
output.writerow(row)
else:
for row in self.rows:
output.writerow(row)
def printerror(res, node=None):
exitcode = 0
if 'errorcode' in res:
exitcode = res['errorcode']
for node in res.get('databynode', {}):
exitcode = res['databynode'][node].get('errorcode', exitcode)
if 'error' in res['databynode'][node]:
sys.stderr.write(
'{0}: {1}\n'.format(node, res['databynode'][node]['error']))
if exitcode == 0:
exitcode = 1
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
return exitcode
def cprint(txt):
try:
print(txt)
except UnicodeEncodeError:
print(txt.encode('utf8'))
sys.stdout.flush()
def _parseserver(string):
if ']:' in string:
server, port = string[1:].split(']:')
elif string[0] == '[':
server = string[1:-1]
port = '13001'
elif ':' in string:
server, port = string.split(':')
else:
server = string
port = '13001'
return server, port
class Command(object):
def __init__(self, server=None):
self._prevdict = None
self._prevkeyname = None
self.connection = None
self._currnoderange = None
self.unixdomain = False
if server is None:
if 'CONFLUENT_HOST' in os.environ:
self.serverloc = os.environ['CONFLUENT_HOST']
else:
self.serverloc = '/var/run/confluent/api.sock'
else:
self.serverloc = server
self.connected = False
async def ensure_connected(self):
if self.connected:
return True
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
self.unixdomain = True
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
await self._connect_tls()
self.protversion = int((await tlvdata.recv(self.connection)).split(
b'--')[1].strip()[1:])
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
else:
self.authenticated = False
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
username = os.environ['CONFLUENT_USER']
passphrase = os.environ['CONFLUENT_PASSPHRASE']
await self.authenticate(username, passphrase)
self.connected = True
async def add_file(self, name, handle, mode):
await self.ensure_connected()
if self.protversion < 3:
raise Exception('Not supported with connected confluent server')
if not self.unixdomain:
raise Exception('Can only add a file to a unix domain connection')
tlvdata.send(self.connection, {'filename': name, 'mode': mode}, handle)
async def authenticate(self, username, password):
await tlvdata.send(self.connection,
{'username': username, 'password': password})
authdata = await tlvdata.recv(self.connection)
if authdata['authpassed'] == 1:
self.authenticated = True
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None, outhandler=None):
if 'error' in res:
if errnodes is not None:
errnodes.add(self._currnoderange)
sys.stderr.write('Error: {0}\n'.format(res['error']))
if 'errorcode' in res:
return res['errorcode']
else:
return 1
if 'databynode' not in res:
return 0
res = res['databynode']
for node in res:
if 'error' in res[node]:
if errnodes is not None:
errnodes.add(node)
sys.stderr.write('{0}: Error: {1}\n'.format(
node, res[node]['error']))
if 'errorcode' in res[node]:
rc |= res[node]['errorcode']
else:
rc |= 1
elif ikey in res[node]:
if 'value' in res[node][ikey]:
val = res[node][ikey]['value']
elif 'isset' in res[node][ikey]:
val = '********' if res[node][ikey] else ''
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
cprint('{0}: {2}->{1}'.format(
node, val, res[node][self._prevkeyname]['value']))
elif self._prevdict and node in self._prevdict:
cprint('{0}: {2}->{1}'.format(
node, val, self._prevdict[node]))
else:
cprint('{0}: {1}'.format(node, val))
elif outhandler:
outhandler(node, res)
return rc
async def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, promptover=None, outhandler=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
async for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
else:
await self.stop_if_noderange_over(noderange, promptover)
kwargs[ikey] = input
async for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res, errnodes, outhandler)
self._currnoderange = None
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def stop_if_noderange_over(self, noderange, maxnodes):
if maxnodes is None:
return
nsize = await self.get_noderange_size(noderange)
if nsize > maxnodes:
if nsize == 1:
nodename = [x async for x in self.read(
'/noderange/{0}/nodes/'.format(noderange))][0].get('item', {}).get('href', None)
nodename = nodename[:-1]
p = getinput('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
else:
p = getinput('Command is about to affect {0} nodes, continue (y/n)? '.format(nsize))
if p.lower() != 'y':
sys.stderr.write('Aborting at user request\n')
sys.exit(1)
raise Exception("Aborting at user request")
async def get_noderange_size(self, noderange):
numnodes = 0
async for node in self.read('/noderange/{0}/nodes/'.format(noderange)):
if node.get('item', {}).get('href', None):
numnodes += 1
else:
raise Exception("Error trying to size noderange {0}".format(noderange))
return numnodes
async def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
for res in await self.read('/nodegroups/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
kwargs[ikey] = input
for res in await self.update('/nodegroups/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
cprint('')
return 0
async def read(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'retrieve', path, self.connection, parameters):
yield rsp
async def update(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'update', path, self.connection, parameters):
yield rsp
async def create(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'create', path, self.connection, parameters):
yield rsp
async def delete(self, path, parameters=None):
await self.ensure_connected()
if not self.authenticated:
raise Exception('Unauthenticated')
async for rsp in send_request(
'delete', path, self.connection, parameters):
yield rsp
def _connect_unix(self):
self.connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.connection.setsockopt(socket.SOL_SOCKET, SO_PASSCRED, 1)
self.connection.connect(self.serverloc)
async def _connect_tls(self):
server, port = _parseserver(self.serverloc)
for res in socket.getaddrinfo(server, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
self.connection = socket.socket(af, socktype, proto)
self.connection.setsockopt(
socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
except:
self.connection = None
continue
try:
self.connection.settimeout(5)
self.connection.connect(sa)
self.connection.settimeout(0)
except:
raise
self.connection.close()
self.connection = None
continue
break
if self.connection is None:
raise Exception("Failed to connect to %s" % self.serverloc)
#TODO(jbjohnso): server certificate validation
clientcfgdir = os.path.join(os.path.expanduser("~"), ".confluent")
try:
os.makedirs(clientcfgdir)
except OSError as exc:
if not (exc.errno == errno.EEXIST and os.path.isdir(clientcfgdir)):
raise
cacert = os.path.join(clientcfgdir, "ca.pem")
certreqs = ssl.CERT_REQUIRED
knownhosts = False
if not os.path.exists(cacert):
cacert = None
certreqs = ssl.CERT_NONE
knownhosts = True
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
ssl_ctx = PySSLContext.from_address(id(ctx)).ctx
libssl.SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_stub, 0)
sreader = asyncio.StreamReader()
sreaderprot = asyncio.StreamReaderProtocol(sreader)
cloop = asyncio.get_event_loop()
tport, _ = await cloop.create_connection(
lambda: sreaderprot, sock=self.connection, ssl=ctx, server_hostname='x')
swriter = asyncio.StreamWriter(tport, sreaderprot, sreader, cloop)
self.connection = (sreader, swriter)
#self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
# cert_reqs=certreqs)
if knownhosts:
certdata = tport.get_extra_info('ssl_object').getpeercert(binary_form=True)
# certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
fingerprint = fingerprint.encode('utf-8')
hostid = '@'.join((port, server))
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
if hostid in khf:
if fingerprint == khf[hostid]:
return
else:
replace = getinput(
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
cprint('Adding new key for %s:%s' % (server, port))
khf[hostid] = fingerprint
async def send_request(operation, path, server, parameters=None):
"""This function iterates over all the responses
received from the server.
:param operation: The operation to request, retrieve, update, delete,
create, start, stop
:param path: The URI path to the resource to operate on
:param server: The socket to send data over
:param parameters: Parameters if any to send along with the request
"""
payload = {'operation': operation, 'path': path}
if parameters is not None:
payload['parameters'] = parameters
await tlvdata.send(server, payload)
result = await tlvdata.recv(server)
while '_requestdone' not in result:
try:
yield result
except GeneratorExit:
while '_requestdone' not in result:
result = await tlvdata.recv(server)
raise
result = await tlvdata.recv(server)
def attrrequested(attr, attrlist, seenattributes, node=None):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = True
return True
elif attr.lower().startswith(candidate.lower() + '.'):
if node is None:
seenattributes.add(truename)
else:
seenattributes[node][truename] = 1
return True
return False
async def printattributes(session, requestargs, showtype, nodetype, noderange, options):
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return await print_attrib_path(path, session, requestargs, options)
def _sort_attrib(k):
if isinstance(k[1], dict) and k[1].get('sortid', None) is not None:
return sortutil.naturalize_string('{}'.format(k[1]['sortid']))
return sortutil.naturalize_string(k[0])
async def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
exitcode = 0
seenattributes = NestedDict()
allnodes = set([])
async for res in session.read(path):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in sorted(res['databynode']):
allnodes.add(node)
for attr, val in sorted(res['databynode'][node].items(), key=_sort_attrib):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
seenattributes[node][attr] = True
if rename:
printattr = rename.get(attr, attr)
else:
printattr = attr
if attrprefix:
printattr = attrprefix + printattr
currattr = res['databynode'][node][attr]
if show_attr(attr, requestargs, seenattributes, options, node):
if 'value' in currattr:
if currattr['value'] is not None:
val = currattr['value']
if isinstance(val, list):
val = ','.join(val)
attrout = '{0}: {1}: {2}'.format(
node, printattr, val).strip()
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node,
printattr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or (isinstance(currattr, dict) and 'broken' in currattr):
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
try:
comparedefault = options.comparedefault
except AttributeError:
comparedefault = False
if comparedefault:
try:
exclude = options.exclude
except AttributeError:
exclude = False
if ((requestargs and not exclude) or
(currattr.get('default', None) is not None and
currattr.get('value', None) is not None and
currattr['value'] != currattr['default'])):
cval = ','.join(currattr['value']) if isinstance(
currattr['value'], list) else currattr['value']
dval = ','.join(currattr['default']) if isinstance(
currattr['default'], list) else currattr['default']
cprint('{0}: {1}: {2} (Default: {3})'.format(
node, printattr, cval, dval))
else:
try:
details = options.detail
except AttributeError:
details = False
if details:
if currattr.get('help', None):
attrout += u' (Help: {0})'.format(
currattr['help'])
if currattr.get('possible', None):
try:
attrout += u' (Choices: {0})'.format(
','.join(currattr['possible']))
except TypeError:
pass
cprint(attrout)
somematched = set([])
printmissing = set([])
badnodes = NestedDict()
if not exitcode:
if requestargs:
for attr in requestargs:
for node in allnodes:
if attr in seenattributes[node]:
somematched.add(attr)
else:
badnodes[node][attr] = True
exitcode = 1
for node in sortutil.natural_sort(badnodes):
for attr in badnodes[node]:
if attr in somematched:
sys.stderr.write(
'Error: {0} matches no valid value for {1}\n'.format(
attr, node))
else:
printmissing.add(attr)
for missing in printmissing:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(missing))
return exitcode
def show_attr(attr, requestargs, seenattributes, options, node):
try:
reverse = options.exclude
except AttributeError:
reverse = False
if requestargs is None or requestargs == []:
return True
processattr = attrrequested(attr, requestargs, seenattributes, node)
if reverse:
processattr = not processattr
return processattr
def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
exitcode = 0
seenattributes = set([])
for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for attr in res:
seenattributes.add(attr)
currattr = res[attr]
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
noderange, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(noderange, attr)
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif isinstance(currattr, dict) and 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(noderange, attr,
currattr['broken'])
elif 'expression' in currattr:
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
return exitcode
async def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
# update attribute
exitcode = 0
if options.clear:
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
keydata = {}
for attrib in updateargs[1:]:
keydata[attrib] = None
async for res in session.update(targpath, keydata):
for node in res.get('databynode', {}):
for warnmsg in res['databynode'][node].get('_warnings', []):
sys.stderr.write('Warning: ' + warnmsg + '\n')
if 'error' in res:
if 'errorcode' in res:
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange,
'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange,
'attributes/all',
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = await session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = await session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
update_ready = True
for arg in updateargs[1:]:
if not '=' in arg:
update_ready = False
exitcode = 1
if not update_ready:
sys.stderr.write('Error: {0} Can not set and read at the same time!\n'.format(str(updateargs[1:])))
sys.exit(exitcode)
try:
for val in updateargs[1:]:
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
value = {'prepend': val[1]}
elif val[0][-1] in ('-', '^'):
value = {'remove': val[1]}
else:
key = val[0]
value = val[1]
if (nodetype == "nodegroups"):
exitcode = await session.simple_nodegroups_command(noderange, 'attributes/all',
value, key)
else:
exitcode = await session.simple_noderange_command(noderange, 'attributes/all',
value, key)
except Exception:
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
exitcode = 1
sys.exit(exitcode)
return exitcode
# So we try to prevent bad things from happening when globbing
# We tried to head this off at the shell, but the various solutions would end
# up breaking the shell in various ways (breaking pipe capability if using
# DEBUG, breaking globbing if in pipe, etc)
# Then we tried to parse the original commandline instead, however shlex isn't
# going to parse full bourne language (e.g. knowing that '|' and '>' and
# a world of other things would not be in our command line
# so finally, just make sure the noderange appears verbatim in the command line
# if we glob to something, then bash will change noderange and this should
# detect it and save the user from tragedy
def check_globbing(noderange):
if not os.path.exists(noderange):
return True
rawargs = os.environ.get('CURRENT_CMDLINE', None)
if rawargs:
rawargs = shlex.split(rawargs)
for arg in rawargs:
if arg.startswith('$'):
arg = arg[1:]
if arg.endswith(';'):
arg = arg[:-1]
arg = os.environ.get(arg, '$' + arg)
if arg.startswith(noderange):
break
else:
sys.stderr.write(
'Shell glob conflict detected, specified target "{0}" '
'not in command line, but is a file. You can use "set -f" in '
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
+323
View File
@@ -0,0 +1,323 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 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.
import array
import asyncio
import ctypes
import ctypes.util
import confluent.tlv as tlv
import socket
from datetime import datetime
import json
import os
import struct
try:
unicode
except NameError:
unicode = str
try:
range = xrange
except NameError:
pass
class iovec(ctypes.Structure): # from uio.h
_fields_ = [('iov_base', ctypes.c_void_p),
('iov_len', ctypes.c_size_t)]
iovec_ptr = ctypes.POINTER(iovec)
class cmsghdr(ctypes.Structure): # also from bits/socket.h
_fields_ = [('cmsg_len', ctypes.c_size_t),
('cmsg_level', ctypes.c_int),
('cmsg_type', ctypes.c_int)]
@classmethod
def init_data(cls, cmsg_len, cmsg_level, cmsg_type, cmsg_data):
Data = ctypes.c_ubyte * ctypes.sizeof(cmsg_data)
class _flexhdr(ctypes.Structure):
_fields_ = cls._fields_ + [('cmsg_data', Data)]
datab = Data(*bytearray(cmsg_data))
return _flexhdr(cmsg_len=cmsg_len, cmsg_level=cmsg_level,
cmsg_type=cmsg_type, cmsg_data=datab)
def CMSG_LEN(length):
sizeof_cmshdr = ctypes.sizeof(cmsghdr)
return ctypes.c_size_t(CMSG_ALIGN(sizeof_cmshdr).value + length)
SCM_RIGHTS = 1
class msghdr(ctypes.Structure): # from bits/socket.h
_fields_ = [('msg_name', ctypes.c_void_p),
('msg_namelen', ctypes.c_uint),
('msg_iov', ctypes.POINTER(iovec)),
('msg_iovlen', ctypes.c_size_t),
('msg_control', ctypes.c_void_p),
('msg_controllen', ctypes.c_size_t),
('msg_flags', ctypes.c_int)]
def CMSG_ALIGN(length): # bits/socket.h
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
return ctypes.c_size_t(ret)
def CMSG_SPACE(length): # bits/socket.h
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
return ctypes.c_size_t(ret)
class ClientFile(object):
def __init__(self, name, mode, fd):
self.fileobject = os.fdopen(fd, mode)
self.filename = name
def _sendmsg(loop, fut, sock, msg, fds, rfd):
if rfd is not None:
loop.remove_reader(rfd)
if fut.cancelled():
return
try:
retdata = sock.sendmsg(
[msg],
[(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds))])
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_reader(fd, _sendmsg, loop, fut, sock, fd)
except Exception as exc:
fut.set_exception(exc)
else:
fut.set_result(retdata)
def send_fds(sock, msg, fds):
cloop = asyncio.get_event_loop()
fut = cloop.create_future()
_sendmsg(cloop, fut, sock, msg, fds, None)
return fut
def _recvmsg(loop, fut, sock, msglen, maxfds, rfd):
if rfd is not None:
loop.remove_reader(rfd)
fds = array.array("i") # Array of ints
try:
msg, ancdata, flags, addr = sock.recvmsg(
msglen, socket.CMSG_LEN(maxfds * fds.itemsize))
except (BlockingIOError, InterruptedError):
fd = sock.fileno()
loop.add_reader(fd, _recvmsg, loop, fut, sock, msglen, maxfds, fd)
except Exception as exc:
fut.set_exception(exc)
else:
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if (cmsg_level == socket.SOL_SOCKET
and cmsg_type == socket.SCM_RIGHTS):
# Append data, ignoring any truncated integers at the end.
fds.frombytes(
cmsg_data[
:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
fut.set_result((msg, list(fds)))
def recv_fds(sock, msglen, maxfds):
cloop = asyncio.get_event_loop()
fut = cloop.create_future()
_recvmsg(cloop, fut, sock, msglen, maxfds, None)
return fut
def decodestr(value):
ret = None
try:
ret = value.decode('utf-8')
except UnicodeDecodeError:
try:
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
except AttributeError:
return value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], bytes):
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(dictdata[key], list):
_unicode_list(dictdata[key])
elif isinstance(dictdata[key], dict):
unicode_dictvalues(dictdata[key])
def _unicode_list(currlist):
for i in range(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
unicode_dictvalues(currlist[i])
elif isinstance(currlist[i], list):
_unicode_list(currlist[i])
async def sendall(handle, data):
if isinstance(handle, tuple):
handle[1].write(data)
return await handle[1].drain()
else:
cloop = asyncio.get_event_loop()
return await cloop.sock_sendall(handle, data)
async def close(handle):
if isinstance(handle, tuple):
handle[1].close()
await handle[1].wait_closed()
else:
handle.close()
async def send(handle, data, filehandle=None):
cloop = asyncio.get_event_loop()
if isinstance(data, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, bytes) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
# if you don't have anything to say, don't say anything at all
return
if tl < 16777216:
# type for string is '0', so we don't need
# to xor anything in
await sendall(handle, struct.pack("!I", tl))
else:
raise Exception("String data length exceeds protocol")
await sendall(handle, data)
elif isinstance(data, dict): # JSON currently only goes to 4 bytes
# Some structured message, like what would be seen in http responses
unicode_dictvalues(data) # make everything unicode, assuming UTF-8
sdata = json.dumps(data, ensure_ascii=False, separators=(',', ':'))
sdata = sdata.encode('utf-8')
tl = len(sdata)
if tl > 16777215:
raise Exception("JSON data exceeds protocol limits")
# xor in the type (0b1 << 24)
if filehandle is None:
tl |= 16777216
await sendall(handle, struct.pack("!I", tl))
await sendall(handle, sdata)
elif isinstance(handle, tuple):
raise Exception("Cannot send filehandle over network socket")
else:
tl |= (2 << 24)
await cloop.sock_sendall(handle, struct.pack("!I", tl))
await send_fds(handle, b'', [filehandle])
async def _grabhdl(handle, size):
if isinstance(handle, tuple):
return await handle[0].read(size)
else:
cloop = asyncio.get_event_loop()
return await cloop.sock_recv(handle, size)
async def recvall(handle, size):
rd = await _grabhdl(handle, size)
while len(rd) < size:
nd = await _grabhdl(handle, size - len(rd))
if not nd:
raise Exception("Error reading data")
rd += nd
return rd
async def recv(handle):
tl = await _grabhdl(handle, 4)
if not tl:
return None
while len(tl) < 4:
ndata = await _grabhdl(handle, 4 - len(tl))
if not ndata:
raise Exception("Error reading data")
tl += ndata
if len(tl) == 0:
return None
tl = struct.unpack("!I", tl)[0]
if tl & 0b10000000000000000000000000000000:
raise Exception("Protocol Violation, reserved bit set")
# 4 byte tlv
dlen = tl & 16777215 # grab lower 24 bits
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
if dlen == 0:
return None
if datatype == tlv.Types.filehandle:
if isinstance(handle, tuple):
raise Exception('Filehandle not supported over TLS socket')
filehandles = array.array('i')
rawbuffer = bytearray(2048)
pkttype = ctypes.c_ubyte * 2048
data = pkttype.from_buffer(rawbuffer)
cmsgsize = CMSG_SPACE(ctypes.sizeof(ctypes.c_int)).value
cmsgarr = bytearray(cmsgsize)
cmtype = ctypes.c_ubyte * cmsgsize
cmsg = cmtype.from_buffer(cmsgarr)
cmsg.cmsg_level = socket.SOL_SOCKET
cmsg.cmsg_type = SCM_RIGHTS
cmsg.cmsg_len = CMSG_LEN(ctypes.sizeof(ctypes.c_int))
iov = iovec()
iov.iov_base = ctypes.addressof(data)
iov.iov_len = 2048
msg = msghdr()
msg.msg_iov = ctypes.pointer(iov)
msg.msg_iovlen = 1
msg.msg_control = ctypes.addressof(cmsg)
msg.msg_controllen = ctypes.sizeof(cmsg)
i = await recv_fds(handle, 2048, 4)
data = i[0]
filehandles = i[1]
data = json.loads(bytes(data))
return ClientFile(data['filename'], data['mode'], filehandles[0])
else:
data = await _grabhdl(handle, dlen)
while len(data) < dlen:
ndata = await _grabhdl(handle, dlen - len(data))
if not ndata:
raise Exception("Error reading data")
data += ndata
if datatype == tlv.Types.text:
return data
elif datatype == tlv.Types.json:
return json.loads(data)
+47 -3
View File
@@ -16,11 +16,55 @@ import fcntl
import sys
import struct
import termios
import select
def get_screengeom():
def get_screengeom(escfallback=False):
# returns height in cells, width in cells, width in pixels, height in pixels
return struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'........'))
geom = list(struct.unpack('hhhh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'........')))
if escfallback and (geom[0] == 0 or geom[1] == 0):
cellgeom = get_cell_geometry_using_esc_18t()
geom[0], geom[1] = cellgeom
if escfallback and (geom[2] == 0 or geom[3] == 0):
pixgeom = get_pixel_geometry_using_esc_14t()
geom[2], geom[3] = pixgeom
return geom
def get_pixel_geometry_using_esc_14t():
sys.stdout.write('\x1b[14t')
sys.stdout.flush()
rlist, _, _ = select.select([sys.stdin], [], [], 1)
if not rlist:
return 0, 0
response = ''
while True:
c = sys.stdin.read(1)
if c == 't':
break
response += c
if not response.startswith('\x1b[4;'):
return 0, 0
pixgeom = response[4:].split(';')
return int(pixgeom[1]), int(pixgeom[0])
def get_cell_geometry_using_esc_18t():
sys.stdout.write('\x1b[18t')
sys.stdout.flush()
rlist, _, _ = select.select([sys.stdin], [], [], 1)
if not rlist:
return 0, 0
response = ''
while True:
c = sys.stdin.read(1)
if c == 't':
break
response += c
if not response.startswith('\x1b[8;'):
return 0, 0
cellgeom = response[4:].split(';')
return int(cellgeom[0]), int(cellgeom[1])
class ScreenPrinter(object):
def __init__(self, noderange, client, textlen=4):
+2 -6
View File
@@ -19,12 +19,8 @@ import array
import ctypes
import ctypes.util
import confluent.tlv as tlv
try:
import eventlet.green.socket as socket
import eventlet.green.select as select
except ImportError:
import socket
import select
import socket
import select
from datetime import datetime
import json
import os
@@ -24,6 +24,8 @@ For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null value.
This is different from setting the value to an empty string.
Arbitrary custom attributes can also be created with the `custom.` prefix.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
+1 -2
View File
@@ -49,8 +49,7 @@ actually be in effect until a reboot.
* `-r COMPONENT`, `--restoredefault=COMPONENT`:
Request that the specified component of the targeted nodes will have its
configuration reset to default. Currently the only component implemented
is uefi.
configuration reset to default. The component may be "uefi" or "bmc".
* `-m MAXNODES`, `--maxnodes=MAXNODES`:
Specify a maximum number of nodes to configure, prompting if over
@@ -11,7 +11,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
## DESCRIPTION
`nodegroupattrip` queries the confluent server to get information about nodes.
`nodegroupattrib` queries the confluent server to get information about nodes.
In the simplest form, it simply takes the given group and lists the attributes of that group.
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
+7
View File
@@ -32,6 +32,13 @@ BMCs map a virtual USB device to that url. Content is loaded on demand, and
as such that URL is referenced potentially once for every IO operation that
the host platform attempts.
## NOTES
When doing an attach of an https:// url, you may hit an error if you have not enrolled your certificate authority.
In a general confluent environment, you can usually address it by:
`# for cert in /var/lib/confluent/public/site/tls/*.pem; do nodecertutil s1-s4 installbmccacert $cert; done`
## OPTIONS
* `-h`, `--help`:
@@ -0,0 +1,74 @@
#!/usr/bin/python3
import glob
import gzip
import base64
import os
import subprocess
import sys
import tempfile
def collect_certificates(tmpdir):
certdata = ''
for cacert in glob.glob(f'{tmpdir}/*.pem'):
with open(cacert, 'r') as f:
certdata += f.read()
return certdata
def embed_certificates(incfg, certdata):
if not certdata:
raise Exception('No certificates found to embed')
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
return incfg
def embed_identity(incfg, identityjson):
incfg = incfg.replace('%IDENTJSON%', identityjson)
return incfg
def embed_apiclient(incfg, apiclient):
with open(apiclient, 'r') as f:
apiclientdata = f.read()
compressed = gzip.compress(apiclientdata.encode())
encoded = base64.b64encode(compressed).decode()
incfg = incfg.replace('%APICLIENTZ64%', encoded)
return incfg
def embed_data(tmpdir, outfile):
templatefile = f'{tmpdir}/bfb.cfg.template'
with open(templatefile, 'r') as f:
incfg = f.read()
certdata = collect_certificates(tmpdir)
incfg = embed_certificates(incfg, certdata)
with open(f'{tmpdir}/identity.json', 'r') as f:
identityjson = f.read()
incfg = embed_identity(incfg, identityjson)
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
with open(outfile, 'w') as f:
f.write(incfg)
def get_identity_json(node):
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
try:
with open(identity_file, 'r') as f:
return f.read()
except FileNotFoundError:
return None
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
sys.exit(1)
node = sys.argv[1]
bfbfile = sys.argv[2]
rshim = sys.argv[3]
os.chdir(os.path.dirname(os.path.abspath(__file__)))
currdir = os.getcwd()
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
@@ -0,0 +1,74 @@
#!/usr/bin/python3
import glob
import gzip
import base64
import os
import subprocess
import sys
import tempfile
def collect_certificates(tmpdir):
certdata = ''
for cacert in glob.glob(f'{tmpdir}/*.pem'):
with open(cacert, 'r') as f:
certdata += f.read()
return certdata
def embed_certificates(incfg, certdata):
if not certdata:
raise Exception('No certificates found to embed')
incfg = incfg.replace('%CONFLUENTCERTCOLL%', certdata)
return incfg
def embed_identity(incfg, identityjson):
incfg = incfg.replace('%IDENTJSON%', identityjson)
return incfg
def embed_apiclient(incfg, apiclient):
with open(apiclient, 'r') as f:
apiclientdata = f.read()
compressed = gzip.compress(apiclientdata.encode())
encoded = base64.b64encode(compressed).decode()
incfg = incfg.replace('%APICLIENTZ64%', encoded)
return incfg
def embed_data(tmpdir, outfile):
templatefile = f'{tmpdir}/bfb.cfg.template'
with open(templatefile, 'r') as f:
incfg = f.read()
certdata = collect_certificates(tmpdir)
incfg = embed_certificates(incfg, certdata)
with open(f'{tmpdir}/identity.json', 'r') as f:
identityjson = f.read()
incfg = embed_identity(incfg, identityjson)
incfg = embed_apiclient(incfg, f'{tmpdir}/../apiclient')
with open(outfile, 'w') as f:
f.write(incfg)
def get_identity_json(node):
identity_file = f'/var/lib/confluent/private/site/identity_files/{node}.json'
try:
with open(identity_file, 'r') as f:
return f.read()
except FileNotFoundError:
return None
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: bfb-autoinstall <node> <bfbfile> <rshim>")
sys.exit(1)
node = sys.argv[1]
bfbfile = sys.argv[2]
rshim = sys.argv[3]
os.chdir(os.path.dirname(os.path.abspath(__file__)))
currdir = os.getcwd()
tempdir = tempfile.mkdtemp(prefix=f'bfb-autoinstall-{node}-')
embed_data(f'{currdir}/{node}', f'{tempdir}/bfb.cfg')
subprocess.check_call(['bfb-install', '-b', bfbfile, '-c', f'{tempdir}/bfb.cfg', '-r', rshim])
@@ -0,0 +1,76 @@
function bfb_modify_os() {
echo 'ubuntu:!' | chpasswd -e
mkdir -p /mnt/opt/confluent/bin/
cat > /mnt/opt/confluent/bin/confluentbootstrap.sh << 'END_OF_EMBED'
#!/bin/bash
cat > /usr/local/share/ca-certificates/confluent.crt << 'END_OF_CERTS'
%CONFLUENTCERTCOLL%
END_OF_CERTS
update-ca-certificates
mkdir -p /opt/confluent/bin /etc/confluent/
cp /usr/local/share/ca-certificates/confluent.crt /etc/confluent/ca.pem
cat > /opt/confluent/bin/apiclient.gz.b64 << 'END_OF_CLIENT'
%APICLIENTZ64%
END_OF_CLIENT
base64 -d /opt/confluent/bin/apiclient.gz.b64 | gunzip > /opt/confluent/bin/apiclient
cat > /etc/confluent/ident.json << 'END_OF_IDENT'
%IDENTJSON%
END_OF_IDENT
python3 /opt/confluent/bin/apiclient -i /etc/confluent/ident.json /confluent-api/self/deploycfg2 > /etc/confluent/confluent.deploycfg
PROFILE=$(grep ^profile: /etc/confluent/confluent.deploycfg |awk '{print $2}')
ROOTPASS=$(grep ^rootpassword: /etc/confluent/confluent.deploycfg | awk '{print $2}'|grep -v null)
if [ -n "$ROOTPASS" ]; then
echo root:$ROOTPASS | chpasswd -e
echo "ubuntu:$ROOTPASS" | chpasswd -e
else
echo 'ubuntu:!' | chpasswd -e
fi
cntmp=$(mktemp -d)
cd "$cntmp" || { echo "Failed to cd to temporary directory $cntmp"; exit 1; }
touch /etc/confluent/confluent.deploycfg
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/confignet > confignet
python3 confignet
cd -
rm -rf "$cntmp"
python3 /opt/confluent/bin/apiclient /confluent-public/os/$PROFILE/scripts/functions > /etc/confluent/functions
bash /etc/confluent/functions run_remote setupssh
for cert in /etc/ssh/ssh*-cert.pub; do
if [ -s $cert ]; then
echo HostCertificate $cert >> /etc/ssh/sshd_config.d/90-confluent.conf
fi
done
mkdir -p /var/log/confluent
chmod 700 /var/log/confluent
touch /var/log/confluent/confluent-firstboot.log
touch /var/log/confluent/confluent-post.log
chmod 600 /var/log/confluent/confluent-post.log
chmod 600 /var/log/confluent/confluent-firstboot.log
exec >> /var/log/confluent/confluent-post.log
exec 2>> /var/log/confluent/confluent-post.log
bash /etc/confluent/functions run_remote_python syncfileclient
bash /etc/confluent/functions run_remote_parts post.d
bash /etc/confluent/functions run_remote_config post.d
exec >> /var/log/confluent/confluent-firstboot.log
exec 2>> /var/log/confluent/confluent-firstboot.log
bash /etc/confluent/functions run_remote_parts firstboot.d
bash /etc/confluent/functions run_remote_config firstboot.d
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: staged'
python3 /opt/confluent/bin/apiclient /confluent-api/self/updatestatus -d 'status: complete'
systemctl disable confluentbootstrap
rm /etc/systemd/system/confluentbootstrap.service
END_OF_EMBED
chmod +x /mnt/opt/confluent/bin/confluentbootstrap.sh
cat > /mnt/etc/systemd/system/confluentbootstrap.service << EOS
[Unit]
Description=First Boot Process
Requires=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/confluent/bin/confluentbootstrap.sh
[Install]
WantedBy=multi-user.target
EOS
chroot /mnt systemctl enable confluentbootstrap
}
@@ -0,0 +1,125 @@
#!/usr/bin/python3
import os
import sys
import tempfile
import glob
import shutil
import shlex
import subprocess
import select
sys.path.append('/opt/lib/confluent/python')
import confluent.sortutil as sortutil
import confluent.client as client
def prep_outdir(node):
tmpdir = tempfile.mkdtemp()
for certfile in glob.glob('/var/lib/confluent/public/site/tls/*.pem'):
basename = os.path.basename(certfile)
destfile = os.path.join(tmpdir, basename)
shutil.copy2(certfile, destfile)
subprocess.check_call(shlex.split(f'confetty set /nodes/{node}/deployment/ident_image=create'))
shutil.copy2(f'/var/lib/confluent/private/identity_files/{node}.json', os.path.join(tmpdir, 'identity.json'))
return tmpdir
def exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller):
remotedir = subprocess.check_output(shlex.split(f'ssh {host} mktemp -d /tmp/bfb.XXXXXX')).decode().strip()
bfbbasename = os.path.basename(bfbfile)
subprocess.check_call(shlex.split(f'rsync -avz --info=progress2 {bfbfile} {host}:{remotedir}/{bfbbasename}'))
subprocess.check_call(shlex.split(f'rsync -avc --info=progress2 /opt/lib/confluent/osdeploy/bluefield/hostscripts/ {host}:{remotedir}/'))
for node in nodetorshim:
rshim = nodetorshim[node]
nodeoutdir = prep_outdir(node)
nodeprofile = subprocess.check_output(shlex.split(f'nodeattrib {node} deployment.pendingprofile')).decode().strip().split(':', 2)[2].strip()
shutil.copy2(f'/var/lib/confluent/public/os/{nodeprofile}/bfb.cfg.template', os.path.join(nodeoutdir, 'bfb.cfg.template'))
subprocess.check_call(shlex.split(f'rsync -avz {nodeoutdir}/ {host}:{remotedir}/{node}/'))
shutil.rmtree(nodeoutdir)
run_cmdv(node, shlex.split(f'ssh {host} sh /etc/confluent/functions confluentpython {remotedir}/bfb-autoinstall {node} {remotedir}/{bfbbasename} {rshim}'), all, poller, pipedesc)
def run_cmdv(node, cmdv, all, poller, pipedesc):
try:
nopen = subprocess.Popen(
cmdv, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except OSError as e:
if e.errno == 2:
sys.stderr.write('{0}: Unable to find local executable file "{1}"\n'.format(node, cmdv[0]))
return
raise
pipedesc[nopen.stdout.fileno()] = {'node': node, 'popen': nopen,
'type': 'stdout', 'file': nopen.stdout}
pipedesc[nopen.stderr.fileno()] = {'node': node, 'popen': nopen,
'type': 'stderr', 'file': nopen.stderr}
all.add(nopen.stdout)
poller.register(nopen.stdout, select.EPOLLIN)
all.add(nopen.stderr)
poller.register(nopen.stderr, select.EPOLLIN)
if __name__ == '__main__':
if len(sys.argv) < 3:
print(f'Usage: {sys.argv[0]} <host> <bfbfile> <node1:rshim1> [<node2:rshim2> ...]')
sys.exit(1)
host = sys.argv[1]
bfbfile = sys.argv[2]
nodetorshim = {}
for arg in sys.argv[3:]:
node, rshim = arg.split(':')
nodetorshim[node] = rshim
installprocs = {}
pipedesc = {}
all = set()
poller = select.epoll()
exec_bfb_install(host, nodetorshim, bfbfile, installprocs, pipedesc, all, poller)
rdy = poller.poll(10)
pendingexecs = []
exitcode = 0
while all:
pernodeout = {}
for r in rdy:
r = r[0]
desc = pipedesc[r]
r = desc['file']
node = desc['node']
data = True
singlepoller = select.epoll()
singlepoller.register(r, select.EPOLLIN)
while data and singlepoller.poll(0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
poller.unregister(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, poller, pipedesc)
singlepoller.close()
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy = poller.poll(10)
@@ -47,38 +47,113 @@ c_crypt.restype = ctypes.c_char_p
def get_my_addresses():
nlhdrsz = struct.calcsize('IHHII')
ifaddrsz = struct.calcsize('BBBBI')
# RTM_GETADDR = 22
# nlmsghdr struct: u32 len, u16 type, u16 flags, u32 seq, u32 pid
nlhdr = struct.pack('IHHII', nlhdrsz + ifaddrsz, 22, 0x301, 0, 0)
# ifaddrmsg struct: u8 family, u8 prefixlen, u8 flags, u8 scope, u32 index
ifaddrmsg = struct.pack('BBBBI', 0, 0, 0, 0, 0)
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((0, 0))
s.sendall(nlhdr + ifaddrmsg)
addrs = []
while True:
pdata = s.recv(65536)
v = memoryview(pdata)
if struct.unpack('H', v[4:6])[0] == 3: # netlink done message
break
while len(v):
length, typ = struct.unpack('IH', v[:6])
if typ == 20:
fam, plen, _, scope, ridx = struct.unpack('BBBBI', v[nlhdrsz:nlhdrsz+ifaddrsz])
if scope in (253, 0):
rta = v[nlhdrsz+ifaddrsz:length]
while len(rta):
rtalen, rtatyp = struct.unpack('HH', rta[:4])
if rtalen < 4:
break
if rtatyp == 1:
addrs.append((fam, rta[4:rtalen], plen, ridx))
rta = rta[msg_align(rtalen):]
v = v[msg_align(length):]
for ifa in get_ifaddrs():
if ifa[0] == 'ip':
addrs.append((ifa[1], ifa[2], ifa[3]))
return addrs
def get_mac_addresses():
macs = []
for ifa in get_ifaddrs():
if ifa[0] == 'ETHER':
macs.append((ifa[1], ifa[2]))
return macs
def get_ifaddrs():
class sockaddr(ctypes.Structure):
_fields_ = [
('sa_family', ctypes.c_uint16),
('sa_data', ctypes.c_ubyte * 14),
]
class sockaddr_in(ctypes.Structure):
_fields_ = [
('sin_family', ctypes.c_uint16),
('sin_port', ctypes.c_uint16),
('sin_addr', ctypes.c_ubyte * 4),
('sin_zero', ctypes.c_ubyte * 8),
]
class sockaddr_in6(ctypes.Structure):
_fields_ = [
('sin6_family', ctypes.c_uint16),
('sin6_port', ctypes.c_uint16),
('sin6_flowinfo', ctypes.c_uint32),
('sin6_addr', ctypes.c_ubyte * 16),
('sin6_scope_id', ctypes.c_uint32),
]
class sockaddr_ll(ctypes.Structure):
_fields_ = [
('sll_family', ctypes.c_uint16),
('sll_protocol', ctypes.c_uint16),
('sll_ifindex', ctypes.c_int32),
('sll_hatype', ctypes.c_uint16),
('sll_pkttype', ctypes.c_uint8),
('sll_halen', ctypes.c_uint8),
('sll_addr', ctypes.c_ubyte * 8),
]
class ifaddrs(ctypes.Structure):
pass
ifaddrs._fields_ = [
('ifa_next', ctypes.POINTER(ifaddrs)),
('ifa_name', ctypes.c_char_p),
('ifa_flags', ctypes.c_uint),
('ifa_addr', ctypes.POINTER(sockaddr)),
('ifa_netmask', ctypes.POINTER(sockaddr)),
('ifa_ifu', ctypes.POINTER(sockaddr)),
('ifa_data', ctypes.c_void_p),
]
libc = ctypes.CDLL(ctypes.util.find_library('c'))
libc.getifaddrs.argtypes = [ctypes.POINTER(ctypes.POINTER(ifaddrs))]
libc.getifaddrs.restype = ctypes.c_int
libc.freeifaddrs.argtypes = [ctypes.POINTER(ifaddrs)]
libc.freeifaddrs.restype = None
ifap = ctypes.POINTER(ifaddrs)()
result = libc.getifaddrs(ctypes.pointer(ifap))
if result != 0:
return []
addresses = []
ifa = ifap
try:
while ifa:
if ifa.contents.ifa_addr:
family = ifa.contents.ifa_addr.contents.sa_family
name = ifa.contents.ifa_name.decode('utf-8') if ifa.contents.ifa_name else None
if family in (socket.AF_INET, socket.AF_INET6):
# skip loopback and non-multicast interfaces
if ifa.contents.ifa_flags & 8 or not ifa.contents.ifa_flags & 0x1000:
ifa = ifa.contents.ifa_next
continue
if family == socket.AF_INET:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in))
addr_bytes = bytes(addr_ptr.contents.sin_addr)
if_index = socket.if_nametoindex(name) if name else 0
addresses.append(('ip', family, addr_bytes, if_index))
elif family == socket.AF_INET6:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_in6))
addr_bytes = bytes(addr_ptr.contents.sin6_addr)
scope_id = addr_ptr.contents.sin6_scope_id
addresses.append(('ip', family, addr_bytes, scope_id))
elif family == socket.AF_PACKET:
addr_ptr = ctypes.cast(ifa.contents.ifa_addr, ctypes.POINTER(sockaddr_ll))
halen = addr_ptr.contents.sll_halen
if addr_ptr.contents.sll_hatype in (1, 32) and halen > 0: # ARPHRD_ETHER or ARPHRD_INFINIBAND
if addr_ptr.contents.sll_hatype == 1 and addr_ptr.contents.sll_addr[0] & 2: # skip locally administered MACs
ifa = ifa.contents.ifa_next
continue
mac_bytes = bytes(addr_ptr.contents.sll_addr[:halen])
macaddr = ':'.join('{:02x}'.format(b) for b in mac_bytes)
addresses.append(('ETHER', name, macaddr))
ifa = ifa.contents.ifa_next
finally:
libc.freeifaddrs(ifap)
return addresses
def scan_confluents(confuuid=None):
srvs = {}
@@ -92,22 +167,24 @@ def scan_confluents(confuuid=None):
s4.bind(('0.0.0.0', 1900))
doneidxs = set([])
msg = 'M-SEARCH * HTTP/1.1\r\nST: urn:xcat.org:service:confluent:'
if not confuuid:
if not confuuid and os.path.exists('/etc/confluent/confluent.deploycfg'):
with open('/etc/confluent/confluent.deploycfg') as dcfg:
for line in dcfg.read().split('\n'):
if line.startswith('confluent_uuid:'):
confluentuuid = line.split(': ')[1]
msg += '/confluentuuid=' + confluentuuid
break
if not confuuid and os.path.exists('/confluent_uuid'):
with open('/confluent_uuid') as cuuidin:
confluentuuid = cuuidin.read().strip()
msg += '/confluentuuid=' + confluentuuid
try:
with open('/sys/devices/virtual/dmi/id/product_uuid') as uuidin:
msg += '/uuid=' + uuidin.read().strip()
except Exception:
pass
for addrf in glob.glob('/sys/class/net/*/address'):
with open(addrf) as addrin:
hwaddr = addrin.read().strip()
msg += '/mac=' + hwaddr
for iface, hwaddr in get_mac_addresses():
msg += '/mac=' + hwaddr
msg = msg.encode('utf8')
for addr in get_my_addresses():
if addr[0] == socket.AF_INET6:
@@ -155,7 +232,8 @@ def scan_confluents(confuuid=None):
if currip.startswith('fe80::') and '%' not in currip:
currip = '{0}%{1}'.format(currip, peer[-1])
srvs[currip] = current
srvlist.append(currip)
if currip not in srvlist:
srvlist.append(currip)
r = select.select((s4, s6), (), (), 2)
if r:
r = r[0]
@@ -356,7 +356,7 @@ class NetworkManager(object):
currteam = deats.get('connection.master', None)
if currteam == team:
return
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname'):
for stg in ('ipv4.dhcp-hostname', 'ipv4.dns', 'ipv6.dns', 'ipv6.dhcp-hostname', 'ipv4.dns-search', 'ipv6.dns-search'):
if deats.get(stg, None):
bondcfg[stg] = deats[stg]
if member in self.uuidbyname:
@@ -488,15 +488,16 @@ if __name__ == '__main__':
continue
myname = s.getsockname()
s.close()
curridx = None
if len(myname) == 4:
curridx = myname[-1]
else:
myname = myname[0]
myname = socket.inet_pton(socket.AF_INET, myname)
for addr in myaddrs:
if myname == addr[1].tobytes():
if myname == addr[1]:
curridx = addr[-1]
if curridx in doneidxs:
if curridx is not None and curridx in doneidxs:
continue
for tries in (1, 2, 3):
try:
@@ -27,7 +27,6 @@ mkdir -p stateless-bin
cp -a el8bin/* .
ln -s el8 el9
ln -s el8 el10
mv el10/initramfs/usr el10/initramfs/var
cp -a debian debian13
mkdir -p debian13/initramfs/usr
mv debian13/initramfs/lib debian13/initramfs/usr/
@@ -86,6 +85,9 @@ cp -a esxi7 esxi8
%install
mkdir -p %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
#cp LICENSE %{buildroot}/opt/confluent/share/licenses/confluent_osdeploy/
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 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/
@@ -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
@@ -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
@@ -68,5 +68,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
@@ -62,5 +62,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))
+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 {
+35 -25
View File
@@ -1,6 +1,7 @@
#!/usr/bin/python2
#!/usr/bin/python3
import argparse
import asyncio
import errno
import os
import pwd
@@ -14,9 +15,9 @@ if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.client as client
import confluent.asynclient as client
import confluent.sortutil as sortutil
import confluent.tlvdata as tlvdata
import confluent.asynctlvdata as tlvdata
try:
input = raw_input
@@ -52,40 +53,47 @@ def make_certificate():
os.umask(umask)
def show_invitation(name, nonvoting=False):
async def show_invitation(name, nonvoting=False):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
clicmd = client.Command()
await clicmd.ensure_connected()
s = clicmd.connection
role = 'nonvoting' if nonvoting else None
tlvdata.send(s, {'collective': {'operation': 'invite', 'name': name, 'role': role}})
invite = tlvdata.recv(s)['collective']
await tlvdata.send(s, {'collective': {'operation': 'invite', 'name': name, 'role': role}})
invite = await tlvdata.recv(s)
invite = invite['collective']
if 'error' in invite:
sys.stderr.write(invite['error'] + '\n')
return
print('{0}'.format(invite['invitation']))
def join_collective(server, invitation):
async def join_collective(server, invitation):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
clicmd = client.Command()
await clicmd.ensure_connected()
s = clicmd.connection
while not invitation:
invitation = input('Paste the invitation here: ')
tlvdata.send(s, {'collective': {'operation': 'join',
await tlvdata.send(s, {'collective': {'operation': 'join',
'invitation': invitation,
'server': server}})
res = tlvdata.recv(s)
res = await tlvdata.recv(s)
res = res.get('collective',
{'status': 'Unknown response: ' + repr(res)})
print(res.get('status', res.get('error', repr(res))))
if 'error' in res:
sys.exit(1)
def delete_member(name):
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'delete',
async def delete_member(name):
clicmd = client.Command()
await clicmd.ensure_connected()
s = clicmd.connection
await tlvdata.send(s, {'collective': {'operation': 'delete',
'member': name}})
res = tlvdata.recv(s)
res = await tlvdata.recv(s)
res = res.get('collective',
{'status': 'Unknown response: ' + repr(res)})
print(res.get('status', res.get('error', repr(res))))
@@ -93,10 +101,12 @@ def delete_member(name):
sys.exit(1)
def show_collective():
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'show'}})
res = tlvdata.recv(s)
async def show_collective():
clicmd = client.Command()
await clicmd.ensure_connected()
s = clicmd.connection
await tlvdata.send(s, {'collective': {'operation': 'show'}})
res = await tlvdata.recv(s)
if 'error' in res:
print(res['error'])
return
@@ -121,7 +131,7 @@ def show_collective():
else:
print('Run collective show on leader for more data')
def main():
async def main():
a = argparse.ArgumentParser(description='Confluent server utility')
sp = a.add_subparsers(dest='command')
gc = sp.add_parser('gencert', help='Generate Confluent Certificates for '
@@ -143,13 +153,13 @@ def main():
if cmdset.command == 'gencert':
make_certificate()
elif cmdset.command == 'invite':
show_invitation(cmdset.name, cmdset.n)
await show_invitation(cmdset.name, cmdset.n)
elif cmdset.command == 'join':
join_collective(cmdset.server, cmdset.i)
await join_collective(cmdset.server, cmdset.i)
elif cmdset.command == 'show':
show_collective()
await show_collective()
elif cmdset.command == 'delete':
delete_member(cmdset.name)
await delete_member(cmdset.name)
if __name__ == '__main__':
main()
asyncio.run(main())
+12 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python2
#!/usr/bin/python3
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
@@ -17,6 +17,9 @@
import sys
import os
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
@@ -30,9 +33,15 @@ import confluent.main
#p.enable()
#try:
import multiprocessing
if __name__ == '__main__':
multiprocessing.freeze_support()
def main():
confluent.main.run(sys.argv)
if __name__ == '__main__':
#multiprocessing.freeze_support()
#asyncio.get_event_loop().run_until_complete(main())
main()
#gt = spawn_for_awaitable(confluent.main.run(sys.argv))
#gt.wait()
#except:
# pass
#p.disable()
+79 -83
View File
@@ -1,6 +1,7 @@
#!/usr/bin/python3
import argparse
import asyncio
import os
import socket
import glob
@@ -16,16 +17,13 @@ import confluent.certutil as certutil
import confluent.client as client
import confluent.config.configmanager as configmanager
import confluent.netutil as netutil
import eventlet.green.subprocess as subprocess
import tempfile
import shutil
import eventlet.green.socket as socket
import eventlet
import greenlet
import pwd
import signal
import confluent.collective.manager as collective
import confluent.noderange as noderange
import subprocess
def check_sysctl_tuning():
with open('/proc/sys/net/ipv4/tcp_sack', 'r') as f:
@@ -76,7 +74,7 @@ def webserver_listening():
return False
def certificates_missing_ips(conn):
async def certificates_missing_ips(conn):
# check if the tls can verify by the right CAs, then further
# check if all ip addresses are in the certificate offered
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -86,9 +84,8 @@ def certificates_missing_ips(conn):
sock = ctx.wrap_socket(conn)
crt = sock.getpeercert()
sans = crt.get('subjectAltName', [])
ips = certutil.get_ip_addresses()
missing_ips = []
for ip in ips:
async for ip in certutil.get_ip_addresses():
for san in sans:
field, val = san
if val[-1] == '\n':
@@ -123,7 +120,6 @@ def web_api_works():
def nics_missing_ipv6():
# check for ability to create AF_INET6, for kernel disabled ipv6
a = socket.socket(socket.AF_INET6)
ipaddrs = subprocess.check_output(['ip', '-br', 'a']).split(b'\n')
missingnics = []
for line in ipaddrs:
@@ -172,15 +168,69 @@ def uuid_matches():
dbuuid = configmanager.get_global('confluent_uuid')
return dbuuid == fsuuid
def lookup_node(node):
async def lookup_node(node):
try:
return socket.getaddrinfo(node, 0)
except greenlet.GreenletExit:
return None
cloop = asyncio.get_event_loop()
return await cloop.getaddrinfo(node, 0)
except Exception:
return None
async def check_ssh_to_node(targsships):
sshutil.ready_keys = {}
sshutil.agent_pid = None
cuser = pwd.getpwnam('confluent')
os.setgid(cuser.pw_gid)
os.setuid(cuser.pw_uid)
await sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
for targ in targsships:
srun = subprocess.run(
['ssh', '-Tn', '-o', 'BatchMode=yes', '-l', 'root',
'-o', 'StrictHostKeyChecking=yes', targ, 'true'],
stdin=subprocess.DEVNULL, stderr=subprocess.PIPE)
if srun.returncode == 0:
print(f'Confluent automation access to {targ} seems OK')
else:
if b'Host key verification failed' in srun.stderr:
emprint(f'Confluent ssh unable to verify host key for {targ}, check /etc/ssh/ssh_known_hosts. (Example resolution: osdeploy initialize -k)')
elif b'ermission denied' in srun.stderr:
emprint(f'Confluent user unable to ssh in to {targ}, check /root/.ssh/authorized_keys on the target system versus /etc/confluent/ssh/automation.pub (Example resolution: osdeploy initialize -a)')
else:
emprint('Unknown error attempting confluent automation ssh:')
sys.stderr.buffer.write(srun.stderr)
if sshutil.agent_pid:
os.kill(int(sshutil.agent_pid), signal.SIGTERM)
sys.exit(0)
if __name__ == '__main__':
async def check_confluent_ssh():
sshutil.ready_keys = {}
sshutil.agent_pid = None
cuser = pwd.getpwnam('confluent')
os.setgid(cuser.pw_gid)
os.setuid(cuser.pw_uid)
fprint('Checking SSH Certificate authority: ')
try:
await sshutil.prep_ssh_key('/etc/confluent/ssh/ca')
print('OK')
except Exception as e:
if type(e).__name__ == 'CalledProcessError' and 'UNPROTECTED' in e.stderr.decode():
emprint('Permissions incorrect on /etc/confluent/ssh/ca (Example resolution: chmod 600 /etc/confluent/ssh/ca)')
else:
emprint('Failed to load SSH authority key, deployed servers will not have host certificates for known_hosts and users may be unable to ssh between nodes without a password (Example resolution: osdeploy initialize -s)')
fprint('Checking confluent SSH automation key: ')
try:
await sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
print('OK')
except Exception as e:
if type(e).__name__ == 'CalledProcessError' and 'UNPROTECTED' in e.stderr.decode():
emprint('Permissions incorrect on /etc/confluent/ssh/automation (Example resolution: chmod 600 /etc/confluent/ssh/automation)')
else:
emprint('Failed to load confluent automation key, syncfiles and profile ansible plays will not work (Example resolution: osdeploy initialize -a)')
if sshutil.agent_pid:
os.kill(int(sshutil.agent_pid), signal.SIGTERM)
sys.exit(0)
async def main():
ap = argparse.ArgumentParser(description='Run configuration checks for a system running confluent service')
ap.add_argument('-n', '--node', help='A node name to run node specific checks against')
ap.add_argument('-a', '--automation', help='Do checks against a deployed node for automation and syncfiles function', action='store_true')
@@ -203,7 +253,7 @@ if __name__ == '__main__':
if conn:
print('Running')
fprint('Web Certificate: ')
cert = certificates_missing_ips(conn)
cert = await certificates_missing_ips(conn)
if cert:
cert = ', '.join(cert)
emprint('Addresses missing from certificate: {0} (Example resolution: osdeploy initialize -t)'.format(cert))
@@ -257,37 +307,8 @@ if __name__ == '__main__':
emprint('No matching public key found for root user (Example resolution: osdeploy initialize -u)')
else:
emprint('No trusted ssh keys for root user, passwordless SSH from managers to nodes may not work (Example resolution: osdeploy initialize -u)')
if sshutil.sshver() > 7.6:
child = os.fork()
if child > 0:
pid, extcode = os.waitpid(child, 0)
else:
sshutil.ready_keys = {}
sshutil.agent_pid = None
cuser = pwd.getpwnam('confluent')
os.setgid(cuser.pw_gid)
os.setuid(cuser.pw_uid)
fprint('Checking SSH Certificate authority: ')
try:
sshutil.prep_ssh_key('/etc/confluent/ssh/ca')
print('OK')
except Exception as e:
if type(e).__name__ == 'CalledProcessError' and 'UNPROTECTED' in e.stderr.decode():
emprint('Permissions incorrect on /etc/confluent/ssh/ca (Example resolution: chmod 600 /etc/confluent/ssh/ca)')
else:
emprint('Failed to load SSH authority key, deployed servers will not have host certificates for known_hosts and users may be unable to ssh between nodes without a password (Example resolution: osdeploy initialize -s)')
fprint('Checking confluent SSH automation key: ')
try:
sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
print('OK')
except Exception as e:
if type(e).__name__ == 'CalledProcessError' and 'UNPROTECTED' in e.stderr.decode():
emprint('Permissions incorrect on /etc/confluent/ssh/automation (Example resolution: chmod 600 /etc/confluent/ssh/automation)')
else:
emprint('Failed to load confluent automation key, syncfiles and profile ansible plays will not work (Example resolution: osdeploy initialize -a)')
if sshutil.agent_pid:
os.kill(int(sshutil.agent_pid), signal.SIGTERM)
sys.exit(0)
if await sshutil.sshver() > 7.6:
subprocess.run([sys.executable, __file__, '--check-ssh'])
fprint('Checking for blocked insecure boot: ')
if insecure_boot_attempts():
emprint('Some nodes are attempting network boot using PXE or HTTP boot, but the node is not configured to allow this (Example resolution: nodegroupattrib everything deployment.useinsecureprotocols=firmware)')
@@ -371,7 +392,7 @@ if __name__ == '__main__':
for nic in glob.glob("/sys/class/net/*/ifindex"):
idx = int(open(nic, "r").read())
nicname = nic.split('/')[-2]
ncfg = netutil.get_nic_config(cfg, args.node, ifidx=idx)
ncfg = await netutil.get_nic_config(cfg, args.node, ifidx=idx)
if ncfg['ipv4_address']:
targsships.append(ncfg['ipv4_address'])
if ncfg['ipv4_address'] or ncfg['ipv4_method'] == 'dhcp':
@@ -392,55 +413,30 @@ if __name__ == '__main__':
if allok:
print(f'No issues detected with attributes of {args.node}')
fprint("Checking name resolution: ")
lk = eventlet.spawn(lookup_node, args.node)
eventlet.sleep(0.1)
tries = 5
while not lk.dead and tries > 0:
eventlet.sleep(1)
tries -= 1
deaddns = False
if not tries:
try:
result = await asyncio.wait_for(lookup_node(args.node), timeout=5)
except asyncio.exceptions.TimeoutError:
emprint('Name resolution takes too long, check state of /etc/resolv.conf and indicated nameservers, this can produce failure to netboot or failure to commence installation')
lk.kill()
deaddns = True
result = lk.wait()
if not result and not deaddns:
emprint('Name resolution failed for node, it is normally a good idea for the node name to resolve to an IP')
if result:
print("OK")
if args.automation:
print(f'Checking confluent automation access to {args.node}...')
child = os.fork()
if child > 0:
pid, extcode = os.waitpid(child, 0)
else:
sshutil.ready_keys = {}
sshutil.agent_pid = None
cuser = pwd.getpwnam('confluent')
os.setgid(cuser.pw_gid)
os.setuid(cuser.pw_uid)
sshutil.prep_ssh_key('/etc/confluent/ssh/automation')
for targ in targsships:
srun = subprocess.run(
['ssh', '-Tn', '-o', 'BatchMode=yes', '-l', 'root',
'-o', 'StrictHostKeyChecking=yes', targ, 'true'],
stdin=subprocess.DEVNULL, stderr=subprocess.PIPE)
if srun.returncode == 0:
print(f'Confluent automation access to {targ} seems OK')
else:
if b'Host key verification failed' in srun.stderr:
emprint(f'Confluent ssh unable to verify host key for {targ}, check /etc/ssh/ssh_known_hosts. (Example resolution: osdeploy initialize -k)')
elif b'ermission denied' in srun.stderr:
emprint(f'Confluent user unable to ssh in to {targ}, check /root/.ssh/authorized_keys on the target system versus /etc/confluent/ssh/automation.pub (Example resolution: osdeploy initialize -a)')
else:
emprint('Unknown error attempting confluent automation ssh:')
sys.stderr.buffer.write(srun.stderr)
if sshutil.agent_pid:
os.kill(int(sshutil.agent_pid), signal.SIGTERM)
sys.exit(0)
subprocess.run([sys.executable, __file__, '--check-ssh'] + targsships)
else:
print("Skipping node checks, no node specified (Example: confluent_selfcheck -n n1)")
# possible checks:
# arping on the node, check for dupes/against nodeinventory?
# arping -D for mgt own ip addresses? check for dupes, also check for bleed through from one nic to another
# iterate through profiles, use mtools to extract site initramfs, check if outdated
if __name__ == '__main__':
if len(sys.argv) >= 2 and sys.argv[1] == '--check-ssh':
if len(sys.argv) >= 3:
asyncio.run(check_ssh_to_node(sys.argv[2:]))
else:
asyncio.run(check_confluent_ssh())
else:
asyncio.run(main())
+6 -5
View File
@@ -16,6 +16,7 @@
# limitations under the License.
import asyncio
import getpass
import optparse
import sys
@@ -79,12 +80,13 @@ if args[0] in ('restore', 'merge'):
# Use the format parameter based on the --yaml option
format = 'yaml' if options.yaml else 'json'
cfm.restore_db_from_directory(
dp = cfm.restore_db_from_directory(
dumpdir, password,
merge="skip" if args[0] == 'merge' else False,
skipped=skipped,
format=format)
asyncio.get_event_loop().run_until_complete(dp)
if skipped['nodes']:
skippedn = ','.join(skipped['nodes'])
print('The following nodes were skipped during merge: '
@@ -93,7 +95,6 @@ if args[0] in ('restore', 'merge'):
skippedn = ','.join(skipped['nodegroups'])
print('The following node groups were skipped during merge: '
'{}'.format(skippedn))
cfm.statelessmode = False
cfm.ConfigManager.wait_for_sync(True)
if owner != 0:
@@ -126,8 +127,8 @@ elif args[0] == 'dump':
# Use the format parameter based on the --yaml option
format = 'yaml' if options.yaml else 'json'
cfm.dump_db_to_directory(dumpdir, password, options.redact,
options.skipkeys, format=format)
dp = cfm.dump_db_to_directory(dumpdir, password, options.redact,
options.skipkeys, format=format)
asyncio.get_event_loop().run_until_complete(dp)
+73 -41
View File
@@ -3,6 +3,7 @@
__author__ = 'jjohnson2,bfinley'
import argparse
import asyncio
import glob
import os
import os.path
@@ -16,10 +17,9 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.collective.manager as collective
import eventlet.green.subprocess as subprocess
import confluent.selfservice as selfservice
import confluent.util as util
import confluent.client as client
import confluent.asynclient as client
import confluent.sshutil as sshutil
import confluent.certutil as certutil
import confluent.netutil as netutil
@@ -36,7 +36,7 @@ def emprint(txt):
print(txt)
fnamechars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.^'
def main(args):
async def main(args):
ap = argparse.ArgumentParser(description='Manage OS deployment resources')
sp = ap.add_subparsers(dest='command')
wiz = sp.add_parser('initialize', help='Do OS deployment preparation')
@@ -66,15 +66,15 @@ def main(args):
if cmdset.command == 'list':
return oslist()
if cmdset.command == 'import':
return osimport(cmdset.imagefile, custname=cmdset.n)
return await osimport(cmdset.imagefile, custname=cmdset.n)
if cmdset.command == 'importcheck':
return osimport(cmdset.imagefile, checkonly=True)
if cmdset.command == 'initialize':
return initialize(cmdset)
return await initialize(cmdset)
if cmdset.command == 'updateboot':
return updateboot(cmdset.profile)
return await updateboot(cmdset.profile)
if cmdset.command == 'rebase':
return rebase(cmdset.profile)
return await rebase(cmdset.profile)
ap.print_help()
def symlinkp(src, trg):
@@ -160,7 +160,7 @@ def init_confluent_myname():
os._exit(0)
def local_node_trust_setup():
async def local_node_trust_setup():
init_confluent_myname()
allnodes, domain = selfservice.get_cluster_list()
myname = collective.get_myname()
@@ -173,7 +173,7 @@ def local_node_trust_setup():
myprincipals.add(myshortname)
if domain:
myprincipals.add('{0}.{1}'.format(myshortname, domain))
for addr in netutil.get_my_addresses():
for addr in await netutil.get_my_addresses():
addr = socket.inet_ntop(addr[0], addr[1])
myprincipals.add(addr)
for pubkey in glob.glob('/etc/ssh/ssh_host_*_key.pub'):
@@ -186,7 +186,12 @@ def local_node_trust_setup():
with open(certfile, 'w') as certout:
certout.write(cert)
if restorecon:
subprocess.check_call(['/usr/sbin/restorecon', certfile])
rcproc = await asyncio.create_subprocess_exec(
'/usr/sbin/restorecon', certfile)
rc = await rcproc.wait()
if rc != 0:
raise Exception("Failure to restorecon")
#subprocess.check_call(['/usr/sbin/restorecon', certfile])
with open('/etc/ssh/sshd_config', 'r') as sshconf:
currconfig = sshconf.read().split('\n')
for conline in currconfig:
@@ -204,12 +209,17 @@ def local_node_trust_setup():
for node in util.natural_sort(allnodes):
equivout.write(node + '\n')
if restorecon:
subprocess.check_call(
['/usr/sbin/restorecon',
'/etc/ssh/shosts.equiv', '/root/.shosts'])
rcproc = await asyncio.create_subprocess_exec(
'/usr/sbin/restorecon', '/etc/ssh/shosts.equiv', '/root/.shosts')
rc = await rcproc.wait()
if rc != 0:
raise Exception('Unable to restorecon')
#subprocess.check_call(
# ['/usr/sbin/restorecon',
# '/etc/ssh/shosts.equiv', '/root/.shosts'])
def install_tftp_content():
async def install_tftp_content():
tftplocation = None
candidates = ('/tftpboot', '/var/lib/tftpboot', '/srv/tftpboot', '/srv/tftp')
for cand in candidates:
@@ -224,7 +234,11 @@ def install_tftp_content():
emprint('/tftpboot is detected as tftp directory, will not try to automatically enable tftp, as it is presumed to be externally managed')
else:
try:
subprocess.check_call(['systemctl', 'enable', 'tftp.socket', '--now'])
tfproc = await asyncio.create_subprocess_exec('systemctl', 'enable', 'tftp.socket', '--now')
rc = await tfproc.wait()
if rc != 0:
raise Exception('{0}'.format(rc))
#subprocess.check_call(['systemctl', 'enable', 'tftp.socket', '--now'])
print('TFTP service is enabled and running')
except Exception:
emprint('Unable to automatically enable and start tftp.socket, tftp server may already be running outside of systemd control')
@@ -251,7 +265,7 @@ def install_tftp_content():
def initialize(cmdset):
async def initialize(cmdset):
if os.getuid() != 0:
sys.stderr.write('This command must run as root user\n')
sys.exit(1)
@@ -300,20 +314,28 @@ def initialize(cmdset):
'passphrase protected ssh key easier.\n')
sys.exit(1)
init_confluent_myname()
sshutil.initialize_root_key(False)
await sshutil.initialize_root_key(False)
if cmdset.t:
didsomething = True
init_confluent_myname()
certutil.create_certificate()
await certutil.create_certificate()
if os.path.exists('/usr/lib/systemd/system/httpd.service'):
try:
subprocess.check_call(['systemctl', 'try-restart', 'httpd'])
hrproc = await asyncio.create_subprocess_exec('systemctl', 'try-restart', 'httpd')
rc = await hrproc.wait()
if rc != 0:
raise Exception('Failed restarting HTTP')
#subprocess.check_call(['systemctl', 'try-restart', 'httpd'])
print('HTTP server has been restarted if it was running')
except Exception:
emprint('New HTTPS certificates generated, restart the web server manually')
elif os.path.exists('/usr/lib/systemd/system/apache2.service'):
try:
subprocess.check_call(['systemctl', 'try-restart', 'apache2'])
hrproc = await asyncio.create_subprocess_exec('systemctl', 'try-restart', 'apache2')
rc = await hrproc.wait()
if rc != 0:
raise Exception('Failed restarting HTTP')
# subprocess.check_call(['systemctl', 'try-restart', 'apache2'])
print('HTTP server has been restarted if it was running')
except Exception:
emprint('New HTTPS certificates generated, restart the web server manually')
@@ -323,20 +345,20 @@ def initialize(cmdset):
didsomething = True
init_confluent_myname()
try:
sshutil.initialize_ca()
await sshutil.initialize_ca()
except sshutil.AlreadyExists:
emprint('Skipping generation of SSH CA, already present and would likely be more problematic to regenerate than to reuse (if absolutely sure you want to discard old CA, then delete /etc/confluent/ssh/ca* and restart confluent)')
if cmdset.a:
didsomething = True
init_confluent_myname()
try:
sshutil.initialize_root_key(True, True)
await sshutil.initialize_root_key(True, True)
except sshutil.AlreadyExists:
emprint('Skipping generation of new automation key, already present and regeneration usually causes more problems. (If absolutely certain, delete /etc/confluent/ssh/automation* and restart confluent)')
if cmdset.p:
install_tftp_content()
await install_tftp_content()
if cmdset.l:
local_node_trust_setup()
await local_node_trust_setup()
if cmdset.k:
cas = set([])
cakeys = set([])
@@ -369,7 +391,7 @@ def initialize(cmdset):
sys.exit(rc)
if not didsomething and (cmdset.k or cmdset.l or cmdset.g or cmdset.p):
if cmdset.g:
updateboot('genesis-x86_64')
await updateboot('genesis-x86_64')
sys.exit(0)
if not didsomething:
sys.stderr.write('Nothing was done, use initialize -i for '
@@ -384,7 +406,7 @@ def initialize(cmdset):
totar = []
if not os.path.exists('confluent_uuid'):
c = client.Command()
for rsp in c.read('/uuid'):
async for rsp in c.read('/uuid'):
uuid = rsp.get('uuid', {}).get('value', None)
if uuid:
oum = os.umask(0o11)
@@ -410,15 +432,20 @@ def initialize(cmdset):
for fname in files:
topack.append(os.path.join(currd, fname))
with open(tmpname, 'wb') as initramfs:
packit = subprocess.Popen(['cpio', '-H', 'newc', '-o'],
stdout=initramfs, stdin=subprocess.PIPE)
packit = await asyncio.subprocess.create_subprocess_exec(
'cpio', '-H', 'newc', '-o',
stdin=asyncio.subprocess.PIPE, stdout=initramfs)
#packit = subprocess.Popen(['cpio', '-H', 'newc', '-o'],
# stdout=initramfs, stdin=subprocess.PIPE)
for packfile in topack:
if not isinstance(packfile, bytes):
packfile = packfile.encode('utf8')
packit.stdin.write(packfile)
packit.stdin.write(b'\n')
await packit.stdin.drain()
packit.stdin.close()
res = packit.wait()
await packit.stdin.wait_closed()
res = await packit.wait()
if res:
sys.stderr.write('Error occurred while packing site initramfs')
sys.exit(1)
@@ -434,11 +461,13 @@ def initialize(cmdset):
finally:
os.umask(oum)
if cmdset.g:
updateboot('genesis-x86_64')
await updateboot('genesis-x86_64')
if totar:
tmptarname = tmpname.replace('cpio', 'tgz')
tarcmd = ['tar', '-czf', tmptarname] + totar
subprocess.check_call(tarcmd)
tarproc = await asyncio.subprocess.create_subprocess_exec(*tarcmd)
await tarproc.wait()
#subprocess.check_call(tarcmd)
os.rename(tmptarname, '/var/lib/confluent/public/site/initramfs.tgz')
oum = os.umask(0o22)
try:
@@ -449,17 +478,19 @@ def initialize(cmdset):
print('Site initramfs content packed successfully')
if not os.path.exists('/etc/confluent/srvcert.pem'):
subprocess.check_call(['collective', 'gencert'])
gcproc = await asyncio.subprocess.create_subprocess_exec('collective', 'gencert')
await gcproc.wait()
#subprocess.check_call(['collective', 'gencert'])
# TODO: check selinux and segetbool for httpd_can_network_connect
# httpd available and enabled?
def updateboot(profilename):
async def updateboot(profilename):
if not os.path.exists('/var/lib/confluent/public/site/initramfs.cpio'):
emprint('Must generate site content first (TLS (-t) and/or SSH (-s))')
return 1
c = client.Command()
for rsp in c.update('/deployment/profiles/{0}'.format(profilename),
async for rsp in c.update('/deployment/profiles/{0}'.format(profilename),
{'updateboot': 1}):
if 'updated' in rsp:
print('Updated: {0}'.format(rsp['updated']))
@@ -467,9 +498,9 @@ def updateboot(profilename):
print(repr(rsp))
def rebase(profilename):
async def rebase(profilename):
c = client.Command()
for rsp in c.update('/deployment/profiles/{0}'.format(profilename), {'rebase': 1}):
async for rsp in c.update('/deployment/profiles/{0}'.format(profilename), {'rebase': 1}):
if 'updated' in rsp:
print('Updated: {0}'.format(rsp['updated']))
elif 'customized' in rsp:
@@ -501,7 +532,7 @@ def oslist():
print("")
def osimport(imagefile, checkonly=False, custname=None):
async def osimport(imagefile, checkonly=False, custname=None):
c = client.Command()
imagefile = os.path.abspath(imagefile)
if c.unixdomain:
@@ -518,7 +549,7 @@ def osimport(imagefile, checkonly=False, custname=None):
apiargs = {'filename': imagefile}
if custname:
apiargs['custname'] = custname
for rsp in c.create(apipath, apiargs):
async for rsp in c.create(apipath, apiargs):
if 'target' in rsp:
importing = True
shortname = rsp['name']
@@ -543,7 +574,7 @@ def osimport(imagefile, checkonly=False, custname=None):
print(repr(rsp))
try:
while importing:
for rsp in c.read('/deployment/importing/{0}'.format(shortname)):
async for rsp in c.read('/deployment/importing/{0}'.format(shortname)):
if 'progress' in rsp:
sys.stdout.write('{0}: {1:.2f}% \r'.format(rsp['phase'],
rsp['progress']))
@@ -563,7 +594,8 @@ def osimport(imagefile, checkonly=False, custname=None):
time.sleep(0.5)
finally:
if shortname:
list(c.delete('/deployment/importing/{0}'.format(shortname)))
async for x in c.delete('/deployment/importing/{0}'.format(shortname)):
pass
if __name__ == '__main__':
main(sys.argv)
asyncio.run(main(sys.argv))
+2 -2
View File
@@ -38,9 +38,9 @@ if [ "$OPKGNAME" = "confluent-server" ]; then
if grep wheezy /etc/os-release; then
sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl, python-msgpack/' debian/control
elif grep jammy /etc/os-release; then
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil/' debian/control
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-aiohttp, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil/' debian/control
else
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil, python3-pyasyncore/' debian/control
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-aiohttp, python3-pyparsing, python3-pyghmi(>=1.5.71), python3-paramiko, python3-pysnmp4, python3-libarchive-c, confluent-vtbufferd, python3-netifaces, python3-yaml, python3-dateutil, python3-pyasyncore/' debian/control
fi
if grep wheezy /etc/os-release; then
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
+3 -3
View File
@@ -34,7 +34,7 @@ import confluent.exceptions as exc
import confluent.lookuptools as lookuptools
import confluent.core
def decode_alert(varbinds, configmanager):
async def decode_alert(varbinds, configmanager):
"""Decode an SNMP alert for a server
Given the agentaddr, OID for the trap, and a dict of varbinds,
@@ -49,11 +49,11 @@ def decode_alert(varbinds, configmanager):
agentaddr = varbinds['.1.3.6.1.6.3.18.1.3.0']
except KeyError:
agentaddr = varbinds['1.3.6.1.6.3.18.1.3.0']
node = lookuptools.node_by_manager(agentaddr)
node = await lookuptools.node_by_manager(agentaddr)
if node is None:
raise exc.InvalidArgumentException(
'Unable to find a node with specified manager')
return confluent.core.handle_path(
return await confluent.core.handle_path(
'/nodes/{0}/events/hardware/decode'.format(node), 'update',
configmanager, varbinds, autostrip=False)
+23 -88
View File
@@ -14,21 +14,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# Overall, the result of this shall be:
# - Web clients can create the same out-of-order responsiveness as socket
# clients (but with more complexity on their end)
# - Web clients can share single request among console sessions
# - Web clients can get async notify of things like node add/remove, events
# This provides an async strategy to http clients. The design is that a http
# session may have an 'async' resource. In such a case, any requests are
# queued and immediately the response is given accepting the queued request.
# A request flags itself as queue-compatible through an HTTP header indicating
# the identifier of the async thread. As responses happen to the queued
# request, data is dispatched to the first registered poller for data on
# the session. This way, a client may elect to provide multiple pollers
# to mitigate general choppiness of http network pattern. It may not be
# worth it, but it's possible.
# This handles ownership of asynchronous behavior driving sessions
# with websockets. There was a long-polling HTTP mechanism but that is removed
# Now it's possible to have asynchronous requests multiplexed over a single websockets
# with none of the "choppiness" inherent to multiple long-polling requests
# Additionally support console session multiplexing, to mitigate needed
# connection count.
@@ -39,16 +28,16 @@
# Much like console sessions, these will be reaped if a client spends too
# far away.
import asyncio
import collections
import confluent.exceptions as exc
import confluent.messages as messages
import confluent.util as util
import eventlet
import greenlet
import confluent.core as core
import confluent.log as log
import time
_asyncsessions = {}
_cleanthread = None
_consolesessions = None
@@ -74,25 +63,14 @@ class AsyncTermRelation(object):
class AsyncSession(object):
def __init__(self, wshandler=None):
def __init__(self, wshandler):
self.asyncid = _assign_asyncid(self)
self.responses = collections.deque()
self.wshandler = wshandler
self._evt = None
self.termrelations = []
self.consoles = set([])
if not wshandler:
self.reaper = eventlet.spawn_after(15, self.destroy)
def add(self, requestid, rsp):
if self.wshandler:
self.wshandler(messages.AsyncMessage((requestid, rsp)))
if self.responses is None:
return
self.responses.append((requestid, rsp))
if self._evt:
self._evt.send()
self._evt = None
async def add(self, requestid, rsp):
await self.wshandler(messages.AsyncMessage((requestid, rsp)))
def set_term_relation(self, env):
# need a term relation to keep track of what data belongs
@@ -107,64 +85,39 @@ class AsyncSession(object):
self.consoles.add(sessionid)
def destroy(self):
if self._evt:
self._evt.send()
self._evt = None
for console in self.consoles:
_consolesessions[console]['session'].destroy()
self.consoles = set([])
self.responses = None
del _asyncsessions[self.asyncid]
def run_handler(self, handler, requestid):
async def run_handler(self, handler, requestid):
try:
for rsp in handler:
self.add(requestid, rsp)
self.add(requestid, messages.AsyncCompletion())
async for rsp in core.iterate_responses(handler):
await self.add(requestid, rsp)
await self.add(requestid, messages.AsyncCompletion())
except Exception as e:
self.add(requestid, e)
print(repr(e))
log.logtrace()
await self.add(requestid, e)
def get_responses(self, timeout=25):
self.reaper.cancel()
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
nextexpiry = time.time() + 90
for csess in list(self.consoles):
try:
_consolesessions[csess]['expiry'] = nextexpiry
except KeyError: # session has been closed elsewhere
self.consoles.discard(csess)
if self._evt:
# TODO(jjohnson2): This precludes the goal of 'double barreled'
# access.... revisit if this could matter
raise Exception('get_responses is not re-entrant')
if not self.responses: # wait to accumulate some
self._evt = eventlet.event.Event()
with eventlet.Timeout(timeout, False):
self._evt.wait()
self._evt = None
while self.responses:
yield self.responses.popleft()
def run_handler(hdlr, env):
asyncsessid = env['HTTP_CONFLUENTASYNCID']
async def run_handler(hdlr, req):
asyncsessid = req.headers['ConfluentAsyncId']
try:
asyncsession = _asyncsessions[asyncsessid]['asyncsession']
requestid = env['HTTP_CONFLUENTREQUESTID']
requestid = req.headers['ConfluentRequestId']
except KeyError:
raise exc.InvalidArgumentException(
'Invalid Session ID or missing request id')
eventlet.spawn_n(asyncsession.run_handler, hdlr, requestid)
cloop = asyncio.get_event_loop()
cloop.create_task(asyncsession.run_handler(hdlr, requestid))
return requestid
def get_async(env, querydict):
global _cleanthread
return _asyncsessions[env['HTTP_CONFLUENTASYNCID']]['asyncsession']
def handle_async(env, querydict, threadset, wshandler=None):
global _cleanthread
# This may be one of two things, a request for a new async stream
# or a request for next data from async stream
# httpapi otherwise handles requests an injecting them to queue
@@ -174,25 +127,7 @@ def handle_async(env, querydict, threadset, wshandler=None):
if wshandler:
yield currsess
return
yield messages.AsyncSession(currsess.asyncid)
return
if querydict['asyncid'] not in _asyncsessions:
raise exc.InvalidArgumentException(
'Invalid or expired async id')
mythreadid = greenlet.getcurrent()
threadset.add(mythreadid)
loggedout = None
currsess = None
try:
currsess = _asyncsessions[querydict['asyncid']]['asyncsession']
for rsp in currsess.get_responses():
yield messages.AsyncMessage(rsp)
except greenlet.GreenletExit as ge:
loggedout = ge
threadset.discard(mythreadid)
if loggedout is not None:
currsess.destroy()
raise exc.LoggedOut()
raise Exception("Long polling asynchttp is discontinued")
def set_console_sessions(consolesessions):
+56 -56
View File
@@ -19,13 +19,13 @@
# the PBKDF2 transform is skipped unless a user has been idle for sufficient
# time
import asyncio
import confluent.config.configmanager as configmanager
import eventlet
import eventlet.tpool
try:
import Cryptodome.Protocol.KDF as KDF
except ImportError:
import Crypto.Protocol.KDF as KDF
from concurrent.futures import ProcessPoolExecutor
from fnmatch import fnmatch
import hashlib
import hmac
@@ -33,6 +33,7 @@ import msgpack
import multiprocessing
import os
import pwd
import confluent.tasks as tasks
import confluent.userutil as userutil
import confluent.util as util
pam = None
@@ -238,7 +239,7 @@ def authorize(name, element, tenant=False, operation='create',
return False
def check_user_passphrase(name, passphrase, operation=None, element=None, tenant=False):
async def check_user_passphrase(name, passphrase, operation=None, element=None, tenant=False):
"""Check a a login name and passphrase for authenticity and authorization
The function combines authentication and authorization into one function.
@@ -268,7 +269,7 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant
# by a user, which might be malicious
# would normally make an event and wait
# but here there's no need for that
eventlet.sleep(0.5)
await asyncio.sleep(0.5)
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None:
@@ -280,7 +281,7 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant
except KeyError:
pass
if ucfg is None:
eventlet.sleep(0.05)
await asyncio.sleep(0.05)
return None
bpassphrase = None
if isinstance(passphrase, dict) and len(passphrase) == 1:
@@ -304,22 +305,16 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant
# throw it at the worker pool when implemented
# maybe a distinct worker pool, wondering about starving out non-auth stuff
salt, crypt = ucfg['cryptpass']
# execute inside tpool to get greenthreads to give it a special thread
# world
# TODO(jbjohnso): util function to generically offload a call
# such a beast could be passed into pyghmi as a way for pyghmi to
# magically get offload of the crypto functions without having
# to explicitly get into the eventlet tpool game
global authworkers
global authcleaner
if authworkers is None:
authworkers = multiprocessing.Pool(processes=1)
authworkers = ProcessPoolExecutor(max_workers=1) # multiprocessing.Pool(processes=1)
else:
authcleaner.cancel()
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
authcleaner = tasks.spawn_task_after(30, _clean_authworkers)
crypted = await _do_pbkdf(passphrase, salt)
del _passchecking[(user, tenant)]
eventlet.sleep(
await asyncio.sleep(
0.05) # either way, we want to stall so that client can't
# determine failure because there is a delay, valid response will
# delay as well
@@ -332,52 +327,56 @@ def check_user_passphrase(name, passphrase, operation=None, element=None, tenant
pwe = pwd.getpwnam(user)
except KeyError:
#pam won't work if the user doesn't exist, don't go further
eventlet.sleep(0.05) # stall even on test for existence of a username
await asyncio.sleep(0.05) # stall even on test for existence of a username
return None
if os.getuid() != 0:
# confluent is running with reduced privilege, however, pam_unix refuses
# to let a non-0 user check anothers password.
# We will fork and the child will assume elevated privilege to
# get unix_chkpwd helper to enable checking /etc/shadow
getprompt, sendprompt = os.pipe()
getprompt, sendprompt = os.fdopen(getprompt, 'rb', 0), os.fdopen(sendprompt, 'wb', 0)
pid = os.fork()
if not pid:
usergood = False
try:
getprompt.close()
# we change to the uid we are trying to authenticate as, because
# pam_unix uses unix_chkpwd which reque
os.setuid(pwe.pw_uid)
pa = pam.pam()
usergood = pa.authenticate(user, passphrase, service=_pamservice)
if (not usergood and len(pa.prompts) > 1 and
(not isinstance(passphrase, dict) or
(set(passphrase) - pa.prompts))):
sendprompt.write(msgpack.packb(list(pa.prompts)))
sendprompt.close()
os._exit(2)
finally:
os._exit(0 if usergood else 1)
sendprompt.close()
usergood = os.waitpid(pid, 0)[1]
if (usergood >> 8) == 2:
prompts = getprompt.read()
if (prompts):
raise PromptsNeeded(msgpack.unpackb(prompts))
usergood = usergood == 0
getprompt.close()
else:
# We are running as root, we don't need to fork in order to authenticate the
# user
usergood = pam.authenticate(user, passphrase, service=_pamservice)
usergood = await asyncio.get_event_loop().run_in_executor(authworkers, pam_check, pwe, user, passphrase)
if usergood:
if bpassphrase:
_passcache[(user, tenant)] = hashlib.sha256(bpassphrase).digest()
return authorize(user, element, tenant, operation, skipuserobj=False)
eventlet.sleep(0.05) # stall even on test for existence of a username
await asyncio.sleep(0.05) # stall even on test for existence of a username
return None
def pam_check(pwe, user, passphrase):
if os.getuid() != 0:
# confluent is running with reduced privilege, however, pam_unix refuses
# to let a non-0 user check anothers password.
# We will fork and the child will assume elevated privilege to
# get unix_chkpwd helper to enable checking /etc/shadow
getprompt, sendprompt = os.pipe()
getprompt, sendprompt = os.fdopen(getprompt, 'rb', 0), os.fdopen(sendprompt, 'wb', 0)
pid = os.fork() # we are forking with asyncio, but we are not using async in the child so it should be fine.
if not pid:
usergood = False
try:
getprompt.close()
# we change to the uid we are trying to authenticate as, because
# pam_unix uses unix_chkpwd which reque
os.setuid(pwe.pw_uid)
pa = pam.pam()
usergood = pa.authenticate(user, passphrase, service=_pamservice)
if (not usergood and len(pa.prompts) > 1 and
(not isinstance(passphrase, dict) or
(set(passphrase) - pa.prompts))):
sendprompt.write(msgpack.packb(list(pa.prompts)))
sendprompt.close()
os._exit(2)
finally:
os._exit(0 if usergood else 1)
sendprompt.close()
usergood = os.waitpid(pid, 0)[1]
if (usergood >> 8) == 2:
prompts = getprompt.read()
if (prompts):
raise PromptsNeeded(msgpack.unpackb(prompts))
usergood = usergood == 0
getprompt.close()
else:
# We are running as root, we don't need to fork in order to authenticate the
# user
usergood = pam.authenticate(user, passphrase, service=_pamservice)
return usergood
def _apply_pbkdf(passphrase, salt):
return KDF.PBKDF2(passphrase, salt, 32, 10000,
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
@@ -390,9 +389,10 @@ def _clean_authworkers():
authcleaner = None
def _do_pbkdf(passphrase, salt):
async def _do_pbkdf(passphrase, salt):
# we must get it over to the authworkers pool or else get blocked in
# compute. However, we do want to wait for result, so we have
# one of the exceedingly rare sort of circumstances where 'apply'
# actually makes sense
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
res = await asyncio.get_event_loop().run_in_executor(authworkers, _apply_pbkdf, passphrase, salt)
return res
+167 -78
View File
@@ -1,11 +1,21 @@
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:
@@ -31,8 +41,8 @@ def normalize_uid():
raise Exception('Need to run as root or owner of /etc/confluent')
return curruid
def get_ip_addresses():
lines, _ = util.run(['ip', 'addr'])
async def get_ip_addresses():
lines, _ = await util.check_output('ip', 'addr')
if not isinstance(lines, str):
lines = lines.decode('utf8')
for line in lines.split('\n'):
@@ -143,11 +153,11 @@ def get_certificate_paths():
tlsmateriallocation.setdefault('bundles', []).append(ngbundlepath)
return tlsmateriallocation
def assure_tls_ca():
async def assure_tls_ca():
keyout, certout = ('/etc/confluent/tls/cakey.pem', '/etc/confluent/tls/cacert.pem')
if not os.path.exists(certout):
#create_simple_ca(keyout, certout)
create_full_ca(certout)
await create_full_ca(certout)
fname = '/var/lib/confluent/public/site/tls/{0}.pem'.format(
collective.get_myname())
ouid = normalize_uid()
@@ -159,8 +169,8 @@ def assure_tls_ca():
raise
try:
shutil.copy2('/etc/confluent/tls/cacert.pem', fname)
hv, _ = util.run(
['openssl', 'x509', '-in', '/etc/confluent/tls/cacert.pem', '-hash', '-noout'])
hv, _ = await util.check_output(
'openssl', 'x509', '-in', '/etc/confluent/tls/cacert.pem', '-hash', '-noout')
if not isinstance(hv, str):
hv = hv.decode('utf8')
hv = hv.strip()
@@ -178,6 +188,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:
@@ -185,7 +206,7 @@ def substitute_cfg(setting, key, val, newval, cfgfile, line):
return True
return False
def create_full_ca(certout):
async def create_full_ca(certout):
mkdirp('/etc/confluent/tls/ca/private')
keyout = '/etc/confluent/tls/ca/private/cakey.pem'
csrout = '/etc/confluent/tls/ca/ca.csr'
@@ -222,24 +243,24 @@ 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')
subprocess.check_call(
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout])
subprocess.check_call(
['openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', subj])
subprocess.check_call(
['openssl', 'ca', '-config', newcfg, '-batch', '-selfsign',
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\nkeyUsage = critical,keyCertSign,cRLSign\n[ca_confluent]\n')
await util.check_call(
'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout)
await util.check_call(
'openssl', 'req', '-new', '-key', keyout, '-out', csrout, '-subj', subj)
await util.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]
keyout, '-out', '/etc/confluent/tls/ca/cacert.pem', '-in', csrout
)
shutil.copy2('/etc/confluent/tls/ca/cacert.pem', certout)
#openssl ca -config openssl.cnf -selfsign -keyfile cakey.pem -startdate 20150214120000Z -enddate 20160214120000Z
#20160107071311Z -enddate 20170106071311Z
def create_simple_ca(keyout, certout):
async def create_simple_ca(keyout, certout):
try:
os.makedirs('/etc/confluent/tls')
except OSError as e:
@@ -249,92 +270,136 @@ def create_simple_ca(keyout, certout):
tmphdl, tmpconfig = tempfile.mkstemp()
os.close(tmphdl)
shutil.copy2(sslcfg, tmpconfig)
subprocess.check_call(
['openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout])
await util.check_call(
'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout)
try:
subj = '/CN=Confluent TLS Certificate authority ({0})'.format(socket.gethostname())
if len(subj) > 68:
subj = subj[:68]
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\n[CACert]\nbasicConstraints = CA:true\n')
subprocess.check_call([
cfgfile.write('\n[CACert]\nbasicConstraints = critical,CA:true\n')
await util.check_call(
'openssl', 'req', '-new', '-x509', '-key', keyout, '-days',
'27300', '-out', certout, '-subj', subj,
'-extensions', 'CACert', '-config', tmpconfig
])
)
finally:
os.remove(tmpconfig)
def create_certificate(keyout=None, certout=None, csrout=None):
if not keyout:
async 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:
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)
cacertname = await 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:
await util.check_call(
'openssl', 'ecparam', '-name', 'secp384r1', '-genkey', '-out',
keyout)
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 = []
async for ip in get_ip_addresses():
ipaddrs.append(ip)
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
])
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')
await util.check_call(
'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')
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,
await util.check_call(
'openssl', 'x509', '-req', '-in', csrfile,
'-CA', '/etc/confluent/tls/cacert.pem',
'-CAkey', '/etc/confluent/tls/cakey.pem',
'-set_serial', serialnum, '-out', certout, '-days', '27300',
'-extfile', extconfig
])
)
else:
# we moved to a 'proper' CA, mainly for access to backdating
# start of certs for finicky system clocks
@@ -348,13 +413,12 @@ 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
])
await util.check_call(
'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:
shutil.copy2(keyout, keycopy)
@@ -381,18 +445,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)
@@ -41,7 +41,7 @@ def check_server_proof(invitation, mycert, peercert, proof):
def check_client_proof(servername, mycert, peercert, proof):
servername = servername.encode('utf-8')
if servername not in pending_invites:
return False
return False, None
invitation = pending_invites[servername]
role = invitation['role']
invitation = invitation['invitation']
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2019 Lenovo
# Copyright 2015-2025 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -408,6 +408,12 @@ node = {
'include /<prefixlen> CIDR suffix to indicate subnet length, which is '
'autodetected by default where possible.',
},
'hardwaremanagement.manager_tls_name': {
'description': 'A name to use in lieu of the value in hardwaremanagement.manager for '
'TLS certificate verification purposes. Some strategies involve a non-IP, '
'non-resolvable name, or this can be used to access by IP while using name-based '
'validation',
},
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
@@ -444,6 +450,9 @@ node = {
#IBM Flex)''',
# 'appliesto': ['system'],
# },
'id.index': {
'description': 'Confluent generated numeric index for the node.',
},
'id.model': {
'description': 'The model number of a node. In scenarios where there '
'is both a name and a model number, it is generally '
@@ -469,17 +478,17 @@ node = {
'the discovery process to decide where to place the mac address of a detected PXE nic.',
},
'net.connection_name': {
'description': 'Name to use when specifiying a name for connection and/or interface name for a team. This may be the name of a team interface, '
'description': 'Name to use when specifiying a name for connection and/or interface name for a team/bond. This may be the name of a team/bond interface, '
'the connection name in network manager for the interface, or may be installed as an altname '
'as supported by the respective OS deployment profiles. Default is to accept default name for '
'a team consistent with the respective OS, or to use the matching original port name as connection name.'
'a team/bond consistent with the respective OS, or to use the matching original port name as connection name.'
},
'net.interface_names': {
'description': 'Interface name or comma delimited list of names to match for this interface. It is generally recommended '
'to leave this blank unless needing to set up interfaces that are not on a common subnet with a confluent server, '
'as confluent servers provide autodetection for matching the correct network definition to an interface. '
'This would be the default name per the deployed OS and can be a comma delimited list to denote members of '
'a team or a single interface for VLAN/PKEY connections.'
'a team/bond or a single interface for VLAN/PKEY connections.'
},
'net.mtu': {
'description': 'MTU to apply to this connection',
@@ -565,7 +574,7 @@ node = {
'operating system',
},
'net.team_mode': {
'description': 'Indicates that this interface should be a team and what mode or runner to use when teamed. '
'description': 'Indicates that this interface should be a team/bond and what mode or runner to use when teamed or bonded. '
'If this covers a deployment interface, one of the member interfaces may be brought up as '
'a standalone interface until deployment is complete, as supported by the OS deployment profile. '
'To support this scenario, the switch should be set up to allow independent operation of member ports (e.g. lacp bypass mode or fallback mode).',
@@ -599,6 +608,10 @@ node = {
'description': ('SNMPv1 community string, it is highly recommended to'
'step up to SNMPv3'),
},
'snmp.privacyprotocol': {
'description': 'The privacy protocol to use for SNMPv3',
'valid_values': ('aes', 'des'),
},
# 'secret.snmplocalizedkey': {
# 'description': ("SNMPv3 key localized to this node's SNMP Engine id"
# 'This can be used in lieu of snmppassphrase to avoid'
File diff suppressed because it is too large Load Diff
+172 -195
View File
@@ -21,6 +21,7 @@
# we track nodes that are actively being logged, watched, or have attached
# there should be no more than one handler per node
import asyncio
import codecs
import collections
import confluent.collective.manager as collective
@@ -29,17 +30,10 @@ import confluent.exceptions as exc
import confluent.interface.console as conapi
import confluent.log as log
import confluent.core as plugin
import confluent.tlvdata as tlvdata
import confluent.asynctlvdata as tlvdata
import confluent.tasks as tasks
import confluent.util as util
import eventlet
import eventlet.event
import eventlet.green.os as os
import eventlet.green.select as select
import eventlet.green.socket as socket
import eventlet.green.subprocess as subprocess
import eventlet.green.ssl as ssl
import eventlet.semaphore as semaphore
import fcntl
import socket
import random
import struct
import time
@@ -60,39 +54,43 @@ def chunk_output(output, n):
for i in range(0, len(output), n):
yield output[i:i + n]
def get_buffer_output(nodename):
async def get_buffer_output(nodename):
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
out.connect("\x00confluent-vtbuffer")
rdr, writer = await asyncio.open_unix_connection(sock=out)
if not isinstance(nodename, bytes):
nodename = nodename.encode('utf8')
outdata = bytearray()
out.send(struct.pack('I', len(nodename)))
out.send(nodename)
select.select((out,), (), (), 30)
writer.write(struct.pack('I', len(nodename)))
writer.write(nodename)
await writer.drain()
while not outdata or outdata[-1]:
try:
chunk = os.read(out.fileno(), 128)
except IOError:
chunk = None
if chunk:
outdata.extend(chunk)
else:
select.select((out,), (), (), 0)
chunk = await rdr.read(128) # os.read(out.fileno(), 128)
if not chunk:
raise Exception("bad read")
outdata.extend(chunk)
writer.close()
await writer.wait_closed()
return bytes(outdata[:-1])
def send_output(nodename, output):
async def send_output(nodename, output):
if not isinstance(nodename, bytes):
nodename = nodename.encode('utf8')
out = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
out.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
out.connect("\x00confluent-vtbuffer")
out.send(struct.pack('I', len(nodename) | (1 << 29)))
out.send(nodename)
rdr, writer = await asyncio.open_unix_connection(sock=out)
hdr = struct.pack('I', len(nodename) | (1 << 29))
writer.write(hdr)
writer.write(nodename)
for chunk in chunk_output(output, 8192):
out.send(struct.pack('I', len(chunk) | (2 << 29)))
out.send(chunk)
writer.write(struct.pack('I', len(chunk) | (2 << 29)))
writer.write(chunk)
await writer.drain()
writer.close()
await writer.wait_closed()
def _utf8_normalize(data, decoder):
# first we give the stateful decoder a crack at the byte stream,
@@ -158,7 +156,6 @@ class ConsoleHandler(object):
# wall clock has gone backwards, use current time as best
# guess
self.lasttime = util.monotonic_time()
self.clearbuffer()
self.reconnect = None
self.users = {}
self._attribwatcher = None
@@ -168,10 +165,14 @@ class ConsoleHandler(object):
if self._genwatchattribs:
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), self._genwatchattribs, self._attribschanged)
self.check_isondemand()
tasks.spawn(self.ondemand_init())
async def ondemand_init(self):
await self.clearbuffer()
await self.check_isondemand()
if not self._isondemand:
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
self._connect()
def resize(self, width, height):
return None
@@ -186,7 +187,7 @@ class ConsoleHandler(object):
retrytime = 120
return retrytime + (retrytime * random.random())
def feedbuffer(self, data):
async def feedbuffer(self, data):
if not isinstance(data, bytes):
data = data.encode('utf-8')
if self.pendingbytes is not None:
@@ -194,16 +195,16 @@ class ConsoleHandler(object):
self.pendingbytes = b''
nodeid = self.termprefix + self.node
try:
send_output(nodeid, data)
await send_output(nodeid, data)
data = self.pendingbytes
self.pendingbytes = None
if data:
send_output(nodeid, data)
await send_output(nodeid, data)
except Exception:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def check_isondemand(self):
async def check_isondemand(self):
self._dologging = True
attrvalue = self.cfgmgr.get_node_attributes(
(self.node,), ('console.logging', 'collective.manager'))
@@ -217,21 +218,21 @@ class ConsoleHandler(object):
self._isondemand = True
if (attrvalue[self.node]['console.logging']['value']) in ('none', 'memory'):
self._dologging = False
self.check_collective(attrvalue)
await self.check_collective(attrvalue)
def check_collective(self, attrvalue):
async def check_collective(self, attrvalue):
myc = attrvalue.get(self.node, {}).get('collective.manager', {}).get(
'value', None)
if list(configmodule.list_collective()) and not myc:
self._is_local = False
self._detach()
self._disconnect()
await self._disconnect()
if myc and myc != collective.get_myname():
# Do not do console connect for nodes managed by another
# confluent collective member
self._is_local = False
self._detach()
self._disconnect()
await self._disconnect()
else:
self._is_local = True
@@ -244,11 +245,11 @@ class ConsoleHandler(object):
return util.monotonic_time() - self.lasttime
return False
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
async def _attribschanged(self, nodeattribs, configmanager, **kwargs):
if 'collective.manager' in nodeattribs[self.node]:
attrval = configmanager.get_node_attributes(self.node,
'collective.manager')
self.check_collective(attrval)
await self.check_collective(attrval)
if 'console.logging' in nodeattribs[self.node]:
# decide whether logging changes how we react or not
self._dologging = True
@@ -271,11 +272,11 @@ class ConsoleHandler(object):
if onlylogging:
return
else:
self._ondemand()
await self._ondemand()
if logvalue in ('none', 'memory'):
self._dologging = False
if not self._isondemand or self.livesessions:
eventlet.spawn(self._connect)
self._connect()
def log(self, *args, **kwargs):
if not self._dologging:
@@ -291,8 +292,8 @@ class ConsoleHandler(object):
else:
self._console.ping()
def clearbuffer(self):
self.feedbuffer(
async def clearbuffer(self):
await self.feedbuffer(
'\x1bc[No data has been received from the remote console since ' \
'connecting. This could\r\nbe due to having the console.logging ' \
'attribute set to none or interactive,\r\nserial console not ' \
@@ -304,40 +305,40 @@ class ConsoleHandler(object):
for ses in list(self.livesessions):
ses.detach()
def _disconnect(self):
async def _disconnect(self):
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread.cancel()
self.connectionthread = None
# clear the terminal buffer when disconnected
self.clearbuffer()
await self.clearbuffer()
if self._console:
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._console.close()
await self._console.close()
self._console = None
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate})
await self._send_rcpts({'connectstate': self.connectstate})
def _ondemand(self):
async def _ondemand(self):
self._isondemand = True
if not self.livesessions and self._console:
self._disconnect()
await self._disconnect()
def _connect(self):
if not self._is_local:
return
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread.cancel()
self.connectionthread = None
self.connectionthread = eventlet.spawn(self._connect_backend)
self.connectionthread = tasks.spawn_task(self._connect_backend())
def _connect_backend(self):
async def _connect_backend(self):
if self._console:
self._console.close()
await self._console.close()
self._console = None
self.connectstate = 'connecting'
self._send_rcpts({'connectstate': self.connectstate})
await self._send_rcpts({'connectstate': self.connectstate})
if self.reconnect:
self.reconnect.cancel()
self.reconnect = None
@@ -345,9 +346,11 @@ class ConsoleHandler(object):
'not configured,\r\nset it to a valid value for console '
'function')
try:
self._console = list(plugin.handle_path(
self._plugin_path.format(self.node),
"create", self.cfgmgr))[0]
consoles = await plugin.handle_path(
self._plugin_path.format(self.node),
"create", self.cfgmgr)
async for cns in consoles:
self._console = cns
except (exc.NotImplementedException, exc.NotFoundException):
self._console = None
except Exception as e:
@@ -358,21 +361,21 @@ class ConsoleHandler(object):
else:
print(traceback.format_exc())
if not isinstance(self._console, conapi.Console):
self.clearbuffer()
await self.clearbuffer()
self.connectstate = 'unconnected'
self.error = 'misconfigured'
self._send_rcpts({'connectstate': self.connectstate,
await self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
self.feedbuffer(
await self.feedbuffer(
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
self._send_rcpts(
await self._send_rcpts(
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
self.clearerror = True
return
if self.clearerror:
self.clearerror = False
self.clearbuffer()
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
await self.clearbuffer()
await self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
self.send_break = self._console.send_break
self.resize = self._console.resize
if self._attribwatcher:
@@ -387,66 +390,66 @@ class ConsoleHandler(object):
(self.node,), attribstowatch, self._attribschanged)
try:
self.resize(width=self.initsize[0], height=self.initsize[1])
self._console.connect(self.get_console_output)
await self._console.connect(self.get_console_output)
except exc.TargetEndpointBadCredentials:
self.clearbuffer()
await self.clearbuffer()
self.error = 'badcredentials'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
await self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
self.reconnect = tasks.spawn_task_after(retrytime, self._connect)
return
except (exc.TargetEndpointUnreachable, socket.gaierror) as se:
self.clearbuffer()
await self.clearbuffer()
self.error = 'unreachable'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
await self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
self.reconnect = tasks.spawn_task_after(retrytime, self._connect)
return
except Exception:
self.clearbuffer()
await self.clearbuffer()
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
self.error = 'unknown'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
await self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
self.reconnect = tasks.spawn_task_after(retrytime, self._connect)
return
self._got_connected()
await self._got_connected()
def _got_connected(self):
async def _got_connected(self):
self.connectstate = 'connected'
self._retrytime = 0
self.log(
logdata='console connected', ltype=log.DataTypes.event,
event=log.Events.consoleconnect)
self._send_rcpts({'connectstate': self.connectstate})
await self._send_rcpts({'connectstate': self.connectstate})
def _got_disconnected(self):
async def _got_disconnected(self):
if self.connectstate != 'unconnected':
self._console.close()
await self._console.close()
self.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._send_rcpts({'connectstate': self.connectstate})
await self._send_rcpts({'connectstate': self.connectstate})
if self._isalive:
self._connect()
else:
self.clearbuffer()
await self.clearbuffer()
def close(self):
async def close(self):
self._isalive = False
self._send_rcpts({'deleting': True})
self._disconnect()
await self._send_rcpts({'deleting': True})
await self._disconnect()
if self._console:
self._console.close()
@@ -458,12 +461,12 @@ class ConsoleHandler(object):
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
def get_console_output(self, data):
async def get_console_output(self, data):
# Spawn as a greenthread, return control as soon as possible
# to the console object
eventlet.spawn(self._handle_console_output, data)
await self._handle_console_output(data)
def attachsession(self, session):
async def attachsession(self, session):
edata = 1
for currsession in self.livesessions:
if currsession.username == session.username:
@@ -473,7 +476,7 @@ class ConsoleHandler(object):
self.log(
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
await self._send_rcpts({'clientcount': len(self.livesessions)})
if self.connectstate == 'unconnected':
# if console is not connected, take time to try to assert
# connectivity now.
@@ -482,11 +485,11 @@ class ConsoleHandler(object):
self.reconnect.cancel()
self.reconnect = None
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
self._connect()
def detachsession(self, session):
async def detachsession(self, session):
edata = 0
self.livesessions.discard(session)
for currsession in self.livesessions:
@@ -497,18 +500,18 @@ class ConsoleHandler(object):
self.log(
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientdisconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
await self._send_rcpts({'clientcount': len(self.livesessions)})
if self._isondemand and not self.livesessions:
self._disconnect()
await self._disconnect()
def reopen(self):
self._got_disconnected()
async def reopen(self):
await self._got_disconnected()
def _handle_console_output(self, data):
async def _handle_console_output(self, data):
if type(data) == int:
if data == conapi.ConsoleEvent.Disconnect:
self._got_disconnected()
await self._got_disconnected()
return
elif data in (b'', u''):
# ignore empty strings from a cconsole provider
@@ -522,23 +525,24 @@ class ConsoleHandler(object):
if self.clearpending or self.clearerror:
self.clearpending = False
self.clearerror = False
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
self._send_rcpts(_utf8_normalize(data, self.utf8decoder))
await self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
await self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
await self._send_rcpts(_utf8_normalize(data, self.utf8decoder))
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
await self.feedbuffer(data)
def _send_rcpts(self, data):
async def _send_rcpts(self, data):
for rcpt in list(self.livesessions):
try:
rcpt.data_handler(data)
except: # No matter the reason, advance to next recipient
await rcpt.data_handler(data)
except Exception as e: # No matter the reason, advance to next recipient
print(repr(e))
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def get_recent(self):
async def get_recent(self):
"""Retrieve 'recent' data
Replay data in the intent to perhaps reproduce the display.
@@ -551,39 +555,41 @@ class ConsoleHandler(object):
'clientcount': len(self.livesessions),
}
nodeid = self.termprefix + self.node
retdata = get_buffer_output(nodeid)
retdata = await get_buffer_output(nodeid)
return retdata, connstate
def write(self, data):
async def write(self, data):
if self.connectstate == 'connected':
try:
if isinstance(data, str) and not isinstance(data, bytes):
data = data.encode('utf-8')
self._console.write(data)
await self._console.write(data)
except Exception:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
self._got_disconnected()
await self._got_disconnected()
def disconnect_node(node, configmanager):
async def disconnect_node(node, configmanager):
consk = (node, configmanager.tenant)
if consk in _handled_consoles:
_handled_consoles[consk].close()
await _handled_consoles[consk].close()
del _handled_consoles[consk]
def _nodechange(added, deleting, renamed, configmanager):
async def _replace_node(old, new, cfm):
await disconnect_node(old, cfm)
await connect_node(new, cfm)
for node in deleting:
eventlet.spawn(disconnect_node, node, configmanager)
tasks.spawn(disconnect_node(node, configmanager))
for node in renamed:
disconnect_node(node, configmanager)
eventlet.spawn(connect_node, renamed[node], configmanager)
tasks.spawn(_replace_node(node, renamed[node], configmanager))
for node in added:
eventlet.spawn(connect_node, node, configmanager)
tasks.spawn(connect_node(node, configmanager))
def _start_tenant_sessions(cfm):
async def _start_tenant_sessions(cfm):
nodeattrs = cfm.get_node_attributes(cfm.list_nodes(), 'collective.manager')
for node in nodeattrs:
manager = nodeattrs[node].get('collective.manager', {}).get('value',
@@ -591,23 +597,26 @@ def _start_tenant_sessions(cfm):
if manager and collective.get_myname() != manager:
continue
try:
connect_node(node, cfm)
await connect_node(node, cfm)
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
cfm.watch_nodecollection(_nodechange)
def initialize():
async 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)
_bufferdaemon = await asyncio.subprocess.create_subprocess_exec(
'/opt/confluent/bin/vtbufferd', 'confluent-vtbuffer')
#_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)
async def start_console_sessions():
await configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager, username=None, direct=True, width=80,
@@ -654,34 +663,36 @@ class ProxyConsole(object):
self.clisession.detach()
self.clisession = None
def relay_data(self):
data = tlvdata.recv(self.remote)
async def relay_data(self):
data = await tlvdata.recv(self.remote)
while data:
self.data_handler(data)
data = tlvdata.recv(self.remote)
self.remote.close()
await self.data_handler(data)
data = await tlvdata.recv(self.remote)
self.remote[1].close()
def get_buffer_age(self):
# the server sends a buffer age if appropriate, no need to handle
# it explicitly in the proxy instance
return False
def get_recent(self):
async def get_recent(self):
# Again, delegate this to the remote collective member
self.skipreplay = False
return b''
def write(self, data):
async def write(self, data):
# Relay data to the collective manager
try:
tlvdata.send(self.remote, data)
except Exception:
await tlvdata.send(self.remote, data)
except Exception as e:
print(repr(e))
raise
if self.clisession:
self.clisession.detach()
await self.clisession.detach()
self.clisession = None
def attachsession(self, session):
async def attachsession(self, session):
self.clisession = session
self.data_handler = session.data_handler
termreq = {
@@ -698,33 +709,26 @@ class ProxyConsole(object):
},
}
try:
remote = socket.create_connection((self.managerinfo['address'], 13001))
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
if not util.cert_matches(self.managerinfo['fingerprint'],
remote.getpeercert(binary_form=True)):
raise Exception('Invalid peer certificate')
remote = await collective.connect_to_collective(None, self.managerinfo['address'])
except Exception as e:
if _tracelog:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
eventlet.sleep(3)
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, event=log.Events.stacktrace)
await asyncio.sleep(3)
if self.clisession:
self.clisession.detach(False)
self.detachsession(None)
await self.clisession.detach(False)
await self.detachsession(None)
return
tlvdata.recv(remote)
tlvdata.recv(remote)
tlvdata.send(remote, termreq)
await tlvdata.recv(remote)
await tlvdata.recv(remote)
await tlvdata.send(remote, termreq)
self.remote = remote
eventlet.spawn(self.relay_data)
tasks.spawn(self.relay_data())
def detachsession(self, session):
async def detachsession(self, session):
# we will disappear, so just let that happen...
if self.remote:
try:
tlvdata.send(self.remote, {'operation': 'stop'})
await tlvdata.send(self.remote, {'operation': 'stop'})
except Exception:
pass
self.clisession = None
@@ -757,8 +761,7 @@ class ConsoleSession(object):
:param configmanager: A configuration manager object for current context
:param username: Username for which this session object will operate
:param datacallback: An asynchronous data handler, to be called when data
is available. Note that if passed, it makes
'get_next_output' non-functional
is available.
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
@@ -780,19 +783,22 @@ class ConsoleSession(object):
self._evt = None
self.node = node
self.write = self.conshdl.write
tasks.spawn(self.delayinit(datacallback, skipreplay))
async def delayinit(self, datacallback, skipreplay):
if datacallback is None:
self.reaper = eventlet.spawn_after(15, self.destroy)
self.reaper = tasks.spawn_task_after(15, self.destroy)
self.databuffer = collections.deque([])
self.data_handler = self.got_data
if not skipreplay:
self.databuffer.extend(self.conshdl.get_recent())
self.databuffer.extend(await self.conshdl.get_recent())
else:
self.data_handler = datacallback
if not skipreplay:
for recdata in self.conshdl.get_recent():
for recdata in await self.conshdl.get_recent():
if recdata:
datacallback(recdata)
self.conshdl.attachsession(self)
await datacallback(recdata)
await self.conshdl.attachsession(self)
def connect_session(self):
@@ -819,7 +825,7 @@ class ConsoleSession(object):
Returns False if no data buffered yet"""
return self.conshdl.get_buffer_age()
def reopen(self):
async def reopen(self):
"""Reopen the session
This can be useful if there is suspicion that the remote console is
@@ -828,27 +834,27 @@ class ConsoleSession(object):
automatically detecting an unusable console in the underlying
technology that cannot be unambiguously autodetected.
"""
self.conshdl.reopen()
await self.conshdl.reopen()
def destroy(self):
async def destroy(self):
if self.registered:
self.conshdl.detachsession(self)
await self.conshdl.detachsession(self)
if self._evt:
self._evt.send()
self._evt = None
self.reghdl = None
def detach(self, reattach=True):
async def detach(self, reattach=True):
"""Handler for the console handler to detach so it can reattach,
currently to facilitate changing from one collective.manager to
another
:return:
"""
self.conshdl.detachsession(self)
await self.conshdl.detachsession(self)
if reattach:
self.connect_session()
self.conshdl.attachsession(self)
await self.conshdl.attachsession(self)
self.write = self.conshdl.write
def got_data(self, data):
@@ -863,32 +869,3 @@ class ConsoleSession(object):
if self._evt:
self._evt.send()
self._evt = None
def get_next_output(self, timeout=45):
"""Poll for next available output on this console.
Ideally purely event driven scheme is perfect. AJAX over HTTP is
at least one case where we don't have that luxury. This function
will not work if the session was initialized with a data callback
instead of polling mode.
"""
self.reaper.cancel()
# postpone death to be 15 seconds after this would timeout
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
if self._evt:
raise Exception('get_next_output is not re-entrant')
if not self.databuffer:
self._evt = eventlet.event.Event()
with eventlet.Timeout(timeout, False):
self._evt.wait()
self._evt = None
if not self.databuffer:
return ""
currdata = self.databuffer.popleft()
if isinstance(currdata, dict):
return currdata
retval = currdata
while self.databuffer and not isinstance(self.databuffer[0], dict):
retval += self.databuffer.popleft()
return retval
+167 -123
View File
@@ -33,10 +33,11 @@
# functions. Console is special and just get's passed through
# see API.txt
import asyncio
import confluent
import confluent.alerts as alerts
import confluent.log as log
import confluent.tlvdata as tlvdata
import confluent.asynctlvdata as tlvdata
import confluent.config.attributes as attrscheme
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
@@ -49,25 +50,17 @@ import confluent.networking.macmap as macmap
import confluent.noderange as noderange
import confluent.osimage as osimage
import confluent.plugin as plugin
import types
try:
import confluent.shellmodule as shellmodule
except ImportError:
pass
try:
import OpenSSL.crypto as crypto
except ImportError:
# Only required for collective mode
crypto = None
import confluent.tasks as tasks
import confluent.util as util
import eventlet
import eventlet.greenpool as greenpool
import eventlet.green.ssl as ssl
import eventlet.queue as queue
import eventlet.semaphore as semaphore
import inspect
import itertools
import msgpack
import os
import eventlet.green.socket as socket
import struct
import sys
import uuid
@@ -76,7 +69,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'raritan', u'raritan', b'affluent', u'affluent', b'cnos', u'cnos', b'enos', u'enos')
PluginCollection = plugin.PluginCollection
@@ -85,6 +78,25 @@ try:
except NameError:
unicode = str
async def iterate_responses(responses):
# normalize plugin behaviors
# First, take care of whatever potentially nested levels of awaitables
# Then handle async generators, generators, then just general iterable types
while inspect.isawaitable(responses):
responses = await responses
if inspect.isasyncgen(responses):
async for rsp in responses:
yield rsp
return
elif inspect.isgenerator(responses) or isinstance(responses, list) or isinstance(responses, tuple):
for rsp in responses:
yield rsp
return
for rsp in responses:
yield rsp
def seek_element(currplace, currkey, depth):
try:
return currplace[currkey]
@@ -175,15 +187,15 @@ class PluginRoute(object):
def handle_storage(configmanager, inputdata, pathcomponents, operation):
async def handle_storage(configmanager, inputdata, pathcomponents, operation):
if len(pathcomponents) == 1:
yield msg.ChildCollection('remote/')
return
if pathcomponents[1] == 'remote':
for rsp in mountmanager.handle_request(configmanager, inputdata, pathcomponents[2:], operation):
async for rsp in mountmanager.handle_request(configmanager, inputdata, pathcomponents[2:], operation):
yield rsp
def handle_deployment(configmanager, inputdata, pathcomponents,
async def handle_deployment(configmanager, inputdata, pathcomponents,
operation):
if len(pathcomponents) == 1:
yield msg.ChildCollection('distributions/')
@@ -221,12 +233,12 @@ def handle_deployment(configmanager, inputdata, pathcomponents,
yield msg.ChildCollection('info')
if operation == 'update':
if 'updateboot' in inputdata:
osimage.update_boot(profname)
await osimage.update_boot(profname)
yield msg.KeyValueData({'updated': profname})
return
elif 'rebase' in inputdata:
try:
updated, customized = osimage.rebase_profile(profname)
updated, customized = await osimage.rebase_profile(profname)
except osimage.ManifestMissing:
raise exc.InvalidArgumentException('Specified profile {0} does not have a manifest.yaml for rebase'.format(profname))
for upd in updated:
@@ -236,7 +248,8 @@ def handle_deployment(configmanager, inputdata, pathcomponents,
return
if pathcomponents[1] == 'fingerprint':
if operation == 'create':
importer = osimage.MediaImporter(inputdata['filename'], configmanager, checkonly=True)
importer = osimage.MediaImporter()
await importer.init(inputdata['filename'], configmanager, checkonly=True)
medinfo = {
'targetpath': importer.targpath,
'name': importer.osname,
@@ -253,10 +266,12 @@ def handle_deployment(configmanager, inputdata, pathcomponents,
return
elif operation == 'create':
if inputdata.get('custname', None):
importer = osimage.MediaImporter(inputdata['filename'],
importer = osimage.MediaImporter()
await importer.init(inputdata['filename'],
configmanager, inputdata['custname'])
else:
importer = osimage.MediaImporter(inputdata['filename'],
importer = osimage.MediaImporter()
await importer.init(inputdata['filename'],
configmanager)
yield msg.KeyValueData({'target': importer.targpath,
'name': importer.importkey})
@@ -300,6 +315,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 +491,15 @@ def _init_core():
}),
'ident_image': PluginRoute({
'handler': 'identimage'
})
}),
'remote_config': {
'run': PluginRoute({
'handler': 'remoteconfig'
}),
'active': PluginCollection({
'handler': 'remoteconfig'
}),
},
},
'events': {
'hardware': {
@@ -620,7 +657,7 @@ def _init_core():
}
def create_user(inputdata, configmanager):
async def create_user(inputdata, configmanager):
try:
username = inputdata['name']
del inputdata['name']
@@ -628,10 +665,10 @@ def create_user(inputdata, configmanager):
del inputdata['role']
except (KeyError, ValueError):
raise exc.InvalidArgumentException('Missing user name or role')
configmanager.create_user(username, role, attributemap=inputdata)
await configmanager.create_user(username, role, attributemap=inputdata)
def create_usergroup(inputdata, configmanager):
async def create_usergroup(inputdata, configmanager):
try:
groupname = inputdata['name']
role = inputdata['role']
@@ -639,18 +676,18 @@ def create_usergroup(inputdata, configmanager):
del inputdata['role']
except (KeyError, ValueError):
raise exc.InvalidArgumentException("Missing user name or role")
configmanager.create_usergroup(groupname, role)
await configmanager.create_usergroup(groupname, role)
def update_usergroup(groupname, attribmap, configmanager):
async def update_usergroup(groupname, attribmap, configmanager):
try:
configmanager.set_usergroup(groupname, attribmap)
await configmanager.set_usergroup(groupname, attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
def update_user(name, attribmap, configmanager):
async def update_user(name, attribmap, configmanager):
try:
configmanager.set_user(name, attribmap)
await configmanager.set_user(name, attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
@@ -682,22 +719,25 @@ def show_user(name, configmanager):
def stripnode(iterablersp, node):
for i in iterablersp:
async def stripnode(iterablersp, node):
async for i in iterate_responses(iterablersp):
if i is None:
raise exc.NotImplementedException("Not Implemented")
if isinstance(i, console.Console):
yield i
continue
i.strip_node(node)
yield i
def iterate_collections(iterable, forcecollection=True):
async def iterate_collections(iterable, forcecollection=True):
for coll in iterable:
if forcecollection and coll[-1] != '/':
coll += '/'
yield msg.ChildCollection(coll, candelete=True)
def iterate_resources(fancydict):
async def iterate_resources(fancydict):
for resource in fancydict:
if resource.startswith("_"):
continue
@@ -726,12 +766,12 @@ def delete_nodegroup_collection(collectionpath, configmanager):
raise Exception("Not implemented")
def delete_node_collection(collectionpath, configmanager, isnoderange):
async def delete_node_collection(collectionpath, configmanager, isnoderange):
if len(collectionpath) == 2: # just node
nodes = [collectionpath[-1]]
if isnoderange:
nodes = noderange.NodeRange(nodes[0], configmanager).nodes
configmanager.del_nodes(nodes)
await configmanager.del_nodes(nodes)
for node in nodes:
yield msg.DeletedResource(node)
else:
@@ -782,7 +822,7 @@ def create_group(inputdata, configmanager):
yield msg.CreatedResource(groupname)
def create_node(inputdata, configmanager):
async def create_node(inputdata, configmanager):
try:
nodename = inputdata['name']
if ' ' in nodename:
@@ -792,13 +832,13 @@ def create_node(inputdata, configmanager):
except KeyError:
raise exc.InvalidArgumentException('name not specified')
try:
configmanager.add_node_attributes(attribmap)
await configmanager.add_node_attributes(attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
yield msg.CreatedResource(nodename)
def create_noderange(inputdata, configmanager):
async def create_noderange(inputdata, configmanager):
try:
noder = inputdata['name']
del inputdata['name']
@@ -808,7 +848,7 @@ def create_noderange(inputdata, configmanager):
except KeyError:
raise exc.InvalidArgumentException('name not specified')
try:
configmanager.add_node_attributes(attribmap)
await configmanager.add_node_attributes(attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
for node in attribmap:
@@ -816,7 +856,7 @@ def create_noderange(inputdata, configmanager):
def enumerate_collections(collections):
async def enumerate_collections(collections):
for collection in collections:
yield msg.ChildCollection(collection)
@@ -896,14 +936,13 @@ def abbreviate_noderange(configmanager, inputdata, operation):
return (msg.KeyValueData({'noderange': noderange.ReverseNodeRange(inputdata['nodes'], configmanager).noderange}),)
def _keepalivefn(connection, xmitlock):
async def _keepalivefn(connection, xmitlock):
while True:
eventlet.sleep(30)
with xmitlock:
await asyncio.sleep(30)
async with xmitlock:
connection.sendall(b'\x00\x00\x00\x00\x00\x00\x00\x01\x00')
def handle_dispatch(connection, cert, dispatch, peername):
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
async def handle_dispatch(connection, cert, dispatch, peername):
if not util.cert_matches(
cfm.get_collective_member(peername)['fingerprint'], cert):
connection.close()
@@ -912,10 +951,11 @@ def handle_dispatch(connection, cert, dispatch, peername):
# We only support msgpack now
# The magic should preclude any pickle, as the first byte can never be
# under 0x20 or so.
connection.close()
connection[1].close()
await connection[1].wait_closed()
return
xmitlock = semaphore.Semaphore()
keepalive = eventlet.spawn(_keepalivefn, connection, xmitlock)
xmitlock = asyncio.Lock()
keepalive = tasks.spawn_task(_keepalivefn(connection, xmitlock))
dispatch = msgpack.unpackb(dispatch[2:], raw=False)
configmanager = cfm.ConfigManager(dispatch['tenant'])
nodes = dispatch['nodes']
@@ -928,15 +968,16 @@ def handle_dispatch(connection, cert, dispatch, peername):
pathcomponents, operation, inputdata, nodes, dispatch['isnoderange'],
configmanager)
except Exception as res:
with xmitlock:
_forward_rsp(connection, res)
keepalive.kill()
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
connection.close()
async with xmitlock:
await _forward_rsp(connection, res)
keepalive.cancel()
connection[1].write('\x00\x00\x00\x00\x00\x00\x00\x00')
await connection[1].drain()
connection[1].close()
await connection[1].wait_closed()
return
plugroute = routespec.routeinfo
nodesbyhandler = {}
passvalues = []
nodeattr = configmanager.get_node_attributes(
nodes, plugroute['pluginattrs'])
for node in nodes:
@@ -957,25 +998,32 @@ def handle_dispatch(connection, cert, dispatch, peername):
else:
nodesbyhandler[hfunc] = [node]
try:
passvalues = asyncio.Queue()
numworkers = 0
for hfunc in nodesbyhandler:
passvalues.append(hfunc(
nodes=nodesbyhandler[hfunc], element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata))
for res in itertools.chain(*passvalues):
with xmitlock:
_forward_rsp(connection, res)
numworkers += 1
tasks.spawn(addtoqueue(passvalues, hfunc, {
'nodes': nodesbyhandler[hfunc],
'element': pathcomponents,
'configmanager': configmanager,
'inputdata': inputdata}))
async for res in iterate_queue(numworkers, passvalues):
async with xmitlock:
await _forward_rsp(connection, res)
except Exception as res:
with xmitlock:
_forward_rsp(connection, res)
keepalive.kill()
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
connection.close()
print("oh noes, " + repr(res))
async with xmitlock:
await _forward_rsp(connection, res)
keepalive.cancel()
connection[1].write(b'\x00\x00\x00\x00\x00\x00\x00\x00')
await connection[1].drain()
connection[1].close()
await connection[1].wait_closed()
def _forward_rsp(connection, res):
async def _forward_rsp(connection, res):
try:
r = res.serialize()
r = res.serialize()
except AttributeError:
if isinstance(res, Exception):
r = msgpack.packb(['Exception', str(res)], use_bin_type=False)
@@ -985,16 +1033,18 @@ def _forward_rsp(connection, res):
use_bin_type=False)
except Exception as e:
r = msgpack.packb(
['Exception', 'Unable to serialize response ' + repr(res) + ' due to ' + str(e)],
use_bin_type=False)
['Exception',
'Unable to serialize response ' + repr(res) + ' due to ' + str(e)],
use_bin_type=False)
rlen = len(r)
if not rlen:
return
connection.sendall(struct.pack('!Q', rlen))
connection.sendall(r)
connection[1].write(struct.pack('!Q', rlen))
connection[1].write(r)
await connection[1].drain()
def handle_node_request(configmanager, inputdata, operation,
async def handle_node_request(configmanager, inputdata, operation,
pathcomponents, autostrip=True):
global vinz
if log.logfull:
@@ -1018,7 +1068,7 @@ def handle_node_request(configmanager, inputdata, operation,
try:
nodeorrange = pathcomponents[1]
if not isnoderange and not configmanager.is_node(nodeorrange):
raise exc.NotFoundException("Invalid Node")
raise exc.NotFoundException(f'Invalid Node: {repr(pathcomponents)}')
if isnoderange and not (len(pathcomponents) == 3 and
pathcomponents[2] == 'abbreviate'):
try:
@@ -1070,13 +1120,13 @@ def handle_node_request(configmanager, inputdata, operation,
if iscollection:
if operation == "delete":
return delete_node_collection(pathcomponents, configmanager,
isnoderange)
isnoderange)
elif operation == "retrieve":
return enumerate_node_collection(pathcomponents, configmanager)
else:
raise Exception("TODO here")
del pathcomponents[0:2]
passvalues = queue.Queue()
passvalues = asyncio.Queue()
plugroute = routespec.routeinfo
_plugin = None
@@ -1096,8 +1146,6 @@ def handle_node_request(configmanager, inputdata, operation,
inputdata=msginputdata)
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
return [passvalue]
else:
return stripnode(passvalue, nodes[0])
elif 'pluginattrs' in plugroute:
@@ -1144,27 +1192,26 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc] = [node]
for bn in badcollnodes:
nodesbyhandler[BadCollective(bn).error] = [bn]
workers = greenpool.GreenPool()
numworkers = 0
for hfunc in nodesbyhandler:
numworkers += 1
workers.spawn(addtoqueue, passvalues, hfunc, {'nodes': nodesbyhandler[hfunc],
tasks.spawn(addtoqueue(passvalues, hfunc, {'nodes': nodesbyhandler[hfunc],
'element': pathcomponents,
'configmanager': configmanager,
'inputdata': _get_input_data(_plugin, pathcomponents,
operation, inputdata,nodes,
isnoderange, configmanager)})
isnoderange, configmanager)}))
for manager in nodesbymanager:
numworkers += 1
workers.spawn(addtoqueue, passvalues, dispatch_request, {
tasks.spawn(addtoqueue(passvalues, dispatch_request, {
'nodes': nodesbymanager[manager], 'manager': manager,
'element': pathcomponents, 'configmanager': configmanager,
'inputdata': inputdata, 'operation': operation, 'isnoderange': isnoderange})
'inputdata': inputdata, 'operation': operation, 'isnoderange': isnoderange}))
if isnoderange or not autostrip:
return iterate_queue(numworkers, passvalues)
return iterate_queue(numworkers, passvalues) # [x async for x in iterate_queue(numworkers, passvalues)]
else:
if numworkers > 0:
return iterate_queue(numworkers, passvalues, nodes[0])
return iterate_queue(numworkers, passvalues, nodes[0]) # [x async for x in iterate_queue(numworkers, passvalues, nodes[0])]
else:
raise exc.NotImplementedException()
@@ -1185,10 +1232,10 @@ def _get_input_data(plugin_ext, pathcomponents, operation, inputdata,
nodes, isnoderange,configmanager)
def iterate_queue(numworkers, passvalues, strip=False):
async def iterate_queue(numworkers, passvalues, strip=False):
completions = 0
while completions < numworkers:
nv = passvalues.get()
nv = await passvalues.get()
if nv == 'theend':
completions += 1
else:
@@ -1199,30 +1246,27 @@ def iterate_queue(numworkers, passvalues, strip=False):
yield nv
def addtoqueue(theq, fun, kwargs):
async def addtoqueue(theq, fun, kwargs):
try:
result = fun(**kwargs)
if isinstance(result, console.Console):
theq.put(result)
await theq.put(result)
else:
for pv in result:
theq.put(pv)
async for pv in iterate_responses(result):
await theq.put(pv)
except Exception as e:
theq.put(e)
await theq.put(e)
finally:
theq.put('theend')
await theq.put('theend')
def dispatch_request(nodes, manager, element, configmanager, inputdata,
async def dispatch_request(nodes, manager, element, configmanager, inputdata,
operation, isnoderange):
a = configmanager.get_collective_member(manager)
try:
remote = socket.create_connection((a['address'], 13001))
remote.settimeout(180)
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
remote = await collective.connect_to_collective(a['fingerprint'], a['address'])
except Exception as e:
raise
for node in nodes:
if a:
yield msg.ConfluentResourceUnavailable(
@@ -1235,10 +1279,7 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
manager))
return
if not util.cert_matches(a['fingerprint'], remote.getpeercert(
binary_form=True)):
raise Exception("Invalid certificate on peer")
banner = tlvdata.recv(remote)
banner = await tlvdata.recv(remote)
vers = banner.split()[2]
if vers == b'v0':
pvers = 2
@@ -1246,17 +1287,18 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
pvers = 4
if sys.version_info[0] < 3:
pvers = 2
tlvdata.recv(remote)
await tlvdata.recv(remote)
myname = collective.get_myname()
dreq = b'\x01\x03' + msgpack.packb(
{'name': myname, 'nodes': list(nodes),
'path': element,'tenant': configmanager.tenant,
'operation': operation, 'inputdata': inputdata, 'isnoderange': isnoderange}, use_bin_type=False)
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote.sendall(dreq)
await tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote[1].write(dreq)
await remote[1].drain()
while True:
try:
rlen = remote.recv(8)
rlen = await remote[0].read(8)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
@@ -1265,7 +1307,7 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
return
while len(rlen) < 8:
try:
nlen = remote.recv(8 - len(rlen))
nlen = await remote[0].read(8 - len(rlen))
except Exception:
nlen = 0
if not nlen:
@@ -1279,7 +1321,7 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
if rlen == 0:
break
try:
rsp = remote.recv(rlen)
rsp = await remote[0].read(rlen)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
@@ -1288,7 +1330,7 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
return
while len(rsp) < rlen:
try:
nrsp = remote.recv(rlen - len(rsp))
nrsp = await remote[0].read(rlen - len(rsp))
except Exception:
nrsp = 0
if not nrsp:
@@ -1315,6 +1357,7 @@ def handle_discovery(pathcomponents, operation, configmanager, inputdata):
if pathcomponents[0] == 'detected':
pass
class Staging:
def __init__(self, user, uuid):
self.uuid_str = uuid
@@ -1362,7 +1405,7 @@ class Staging:
raise FileNotFoundError
return directory
def handle_staging(pathcomponents, operation, configmanager, inputdata):
async def handle_staging(pathcomponents, operation, configmanager, inputdata):
'''
e.g push_url: /confluent-api/staging/user/<unique_id>
'''
@@ -1391,10 +1434,11 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata):
with open(file, 'wb') as f:
while remaining_length > 0:
progress = (1 - (remaining_length/content_length)) * 100
#TODO: ASYNC Need to change to aiohttp approach
datachunk = filedata['wsgi.input'].read(min(chunk_size, remaining_length))
f.write(datachunk)
remaining_length -= len(datachunk)
eventlet.sleep(0)
await asyncio.sleep(0)
yield msg.FileUploadProgress(progress)
yield msg.FileUploadProgress(100)
@@ -1406,7 +1450,7 @@ def handle_staging(pathcomponents, operation, configmanager, inputdata):
else:
raise Exception("Invalid url")
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
async def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
"""Given a full path request, return an object.
The plugins should generally return some sort of iterator.
@@ -1420,7 +1464,7 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
if not pathcomponents: # root collection list
return enumerate_collections(rootcollections)
elif pathcomponents[0] == 'noderange':
return handle_node_request(configmanager, inputdata, operation,
return await handle_node_request(configmanager, inputdata, operation,
pathcomponents, autostrip)
elif pathcomponents[0] == 'deployment':
return handle_deployment(configmanager, inputdata, pathcomponents,
@@ -1434,13 +1478,13 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
operation)
elif pathcomponents[0] == 'nodes':
# single node request of some sort
return handle_node_request(configmanager, inputdata,
return await handle_node_request(configmanager, inputdata,
operation, pathcomponents, autostrip)
elif pathcomponents[0] == 'discovery':
return disco.handle_api_request(
return await disco.handle_api_request(
configmanager, inputdata, operation, pathcomponents)
elif pathcomponents[0] == 'networking':
return macmap.handle_api_request(
return await macmap.handle_api_request(
configmanager, inputdata, operation, pathcomponents)
elif pathcomponents[0] == 'version':
return (msg.Attributes(kv={'version': confluent.__version__}),)
@@ -1460,7 +1504,7 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata,
configmanager=configmanager)
create_usergroup(inputdata.attribs, configmanager)
await create_usergroup(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_usergroups(),
forcecollection=False)
if usergroup not in configmanager.list_usergroups():
@@ -1473,7 +1517,7 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata,
configmanager=configmanager)
update_usergroup(usergroup, inputdata.attribs, configmanager)
await update_usergroup(usergroup, inputdata.attribs, configmanager)
return show_usergroup(usergroup, configmanager)
elif pathcomponents[0] == 'users':
# TODO: when non-administrator accounts exist,
@@ -1485,7 +1529,7 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata,
configmanager=configmanager)
create_user(inputdata.attribs, configmanager)
await create_user(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_users(),
forcecollection=False)
if user not in configmanager.list_users():
@@ -1510,11 +1554,11 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
if element != 'decode':
raise exc.NotFoundException()
if operation == 'update':
return alerts.decode_alert(inputdata, configmanager)
return await alerts.decode_alert(inputdata, configmanager)
elif pathcomponents[0] == 'discovery':
return handle_discovery(pathcomponents[1:], operation, configmanager,
inputdata)
elif pathcomponents[0] == 'staging':
return handle_staging(pathcomponents, operation, configmanager, inputdata)
return await handle_staging(pathcomponents, operation, configmanager, inputdata)
else:
raise exc.NotFoundException()
+38 -32
View File
@@ -14,14 +14,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.config.configmanager as cfm
import confluent.netutil as netutil
import confluent.util as util
import datetime
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
import eventlet.greenpool
import hashlib
import hmac
import os
@@ -42,10 +39,10 @@ libc = ctypes.CDLL(ctypes.util.find_library('c'))
# 128, len, len, key - sealed key
def address_is_somewhat_trusted(address, nodename, cfm):
if netutil.ip_on_same_subnet(address.split('%')[0], 'fe80::', 64):
async def address_is_somewhat_trusted(address, nodename, cfm):
if await netutil.ip_on_same_subnet(address.split('%')[0], 'fe80::', 64):
return True
if netutil.address_is_local(address):
if await netutil.address_is_local(address):
return True
authnets = cfm.get_node_attributes(nodename, 'trusted.subnets')
authnets = authnets.get(nodename, {}).get('trusted.subnets', {}).get('value', None)
@@ -54,7 +51,7 @@ def address_is_somewhat_trusted(address, nodename, cfm):
for anet in authnet.split():
na, plen = anet.split('/')
plen = int(plen)
if netutil.ip_on_same_subnet(address, na, plen):
if await netutil.ip_on_same_subnet(address, na, plen):
return True
return False
@@ -62,31 +59,33 @@ class CredServer(object):
def __init__(self):
self.cfm = cfm.ConfigManager(None)
def handle_client(self, client, peer):
async def handle_client(self, client, peer):
disarm = None
try:
apiarmed = None
hmackey = None
hmacval = None
client.send(b'\xc2\xd1-\xa8\x80\xd8j\xba')
tlv = bytearray(client.recv(2))
cloop = asyncio.get_event_loop()
await cloop.sock_sendall(client, b'\xc2\xd1-\xa8\x80\xd8j\xba')
tlv = bytearray(await cloop.sock_recv(client, 2))
if tlv[0] != 1:
client.close()
return
nodename = util.stringify(client.recv(tlv[1]))
tlv = bytearray(client.recv(2)) # should always be null
nodename = util.stringify(await cloop.sock_recv(client, tlv[1]))
tlv = bytearray(await cloop.sock_recv(client, 2)) # should always be null
onlylocal = True
if tlv[0] == 6:
hmacval = client.recv(tlv[1])
hmacval = await cloop.sock_recv(client, tlv[1])
hmackey = self.cfm.get_node_attributes(nodename, ['secret.selfapiarmtoken'], decrypt=True)
hmackey = hmackey.get(nodename, {}).get('secret.selfapiarmtoken', {}).get('value', None)
elif tlv[1]:
client.recv(tlv[1])
await cloop.sock_recv(client, tlv[1])
apimats = self.cfm.get_node_attributes(nodename,
['deployment.apiarmed', 'deployment.sealedapikey'])
apiarmed = apimats.get(nodename, {}).get('deployment.apiarmed', {}).get(
'value', None)
if not hmackey:
if not address_is_somewhat_trusted(peer[0], nodename, self.cfm):
if not await address_is_somewhat_trusted(peer[0], nodename, self.cfm):
client.close()
return
if not apiarmed:
@@ -97,7 +96,7 @@ class CredServer(object):
if not isinstance(sealed, bytes):
sealed = sealed.encode('utf8')
reply = b'\x80' + struct.pack('>H', len(sealed) + 1) + sealed + b'\x00'
client.send(reply)
await cloop.sock_sendall(client, reply)
client.close()
return
if apiarmed not in ('once', 'continuous'):
@@ -107,41 +106,48 @@ class CredServer(object):
self.cfm.set_node_attributes({nodename: {'deployment.apiarmed': ''}})
client.close()
return
client.send(b'\x02\x20')
await cloop.sock_sendall(client, b'\x02\x20')
rttoken = os.urandom(32)
client.send(rttoken)
client.send(b'\x00\x00')
tlv = bytearray(client.recv(2))
await cloop.sock_sendall(client, rttoken)
await cloop.sock_sendall(client, b'\x00\x00')
tlv = bytearray(await cloop.sock_recv(client, 2))
if tlv[0] != 3:
client.close()
return
echotoken = client.recv(tlv[1])
echotoken = await cloop.sock_recv(client, tlv[1])
if echotoken != rttoken:
client.close()
return
tlv = bytearray(client.recv(2))
tlv = bytearray(await cloop.sock_recv(client, 2))
if tlv[0] != 4:
client.close()
return
echotoken = util.stringify(client.recv(tlv[1]))
echotoken = util.stringify(await cloop.sock_recv(client, tlv[1]))
if hmackey:
etok = echotoken.encode('utf8')
if hmacval != hmac.new(hmackey, etok, hashlib.sha256).digest():
client.close()
return
cfgupdate = {nodename: {'crypted.selfapikey': {'hashvalue': echotoken}}}
self.cfm.set_node_attributes(cfgupdate)
client.recv(2) # drain end of message
client.send(b'\x05\x00') # report success
await self.cfm.set_node_attributes(cfgupdate)
await cloop.sock_recv(client, 2) # drain end of message
await cloop.sock_sendall(client, b'\x05\x00') # report success
if hmackey and apiarmed != 'continuous':
self.cfm.clear_node_attributes([nodename], ['secret.selfapiarmtoken'])
if apiarmed != 'continuous':
tokclear = {nodename: {'deployment.sealedapikey': '', 'deployment.apiarmed': ''}}
self.cfm.set_node_attributes(tokclear)
disarm = {nodename: {'deployment.sealedapikey': '', 'deployment.apiarmed': ''}}
finally:
client.close()
try:
client.close()
except Exception:
pass
if disarm:
await self.cfm.set_node_attributes(disarm)
if __name__ == '__main__':
async def main():
a = CredServer()
while True:
eventlet.sleep(86400)
await asyncio.sleep(86400)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
+79
View File
@@ -0,0 +1,79 @@
import asyncio
import code
import os
import socket
import sys
import confluent.tasks as tasks
#this will ultimately fill the role of the 'backdoor' of eventlet
# since we have to asyncio up the input and output, we use InteractiveInterpreter and handle the
# input ourselves, since code is not asyncio friendly in and of itself
#code.InteractiveConsole().interact()
async def interact(cloop, cnn):
prompt = b'>>> '
somecode = ''
itr = code.InteractiveInterpreter()
confile = cnn.makefile('rw')
while True:
await cloop.sock_sendall(cnn, prompt)
prompt = b'... '
newinput = b''
while b'\n' not in newinput:
rcv = await cloop.sock_recv(cnn, 4)
if not rcv:
return
newinput += rcv
somecode += newinput.decode()
if newinput.startswith(b' '):
prompt = b'... '
continue
try:
compcode = code.compile_command(somecode)
except SyntaxError as e:
await cloop.sock_sendall(cnn, repr(e).encode('utf8'))
await cloop.sock_sendall(cnn, b'\n')
compcode = None
somecode = ''
prompt = b'>>> '
if compcode:
saved = sys.stdin, sys.stderr, sys.stdout
try:
cnn.settimeout(10)
confile = cnn.makefile('rw')
sys.stderr = sys.stdout = confile
itr.runcode(compcode)
confile.flush()
finally:
sys.stdin, sys.stderr, sys.stdout = saved
cnn.settimeout(0)
somecode = ''
prompt = b'>>> '
async def srv_debug(sock):
cloop = asyncio.get_event_loop()
while True:
cnn, addr = await cloop.sock_accept(sock)
tasks.spawn(interact(cloop, cnn))
def start_dbgif():
unixsocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
unixsocket.settimeout(0)
try:
os.remove("/var/run/confluent/dbg.sock")
except OSError: # if file does not exist, no big deal
pass
if not os.path.isdir("/var/run/confluent"):
os.makedirs('/var/run/confluent', 0o755)
oumask = os.umask(0o077)
unixsocket.bind("/var/run/confluent/dbg.sock")
unixsocket.listen(12)
os.chmod("/var/run/confluent/dbg.sock",
0o600)
os.umask(oumask)
tasks.spawn(srv_debug(unixsocket))
+141 -110
View File
@@ -61,6 +61,7 @@
# retry until uppercase, lowercase, digit, and symbol all present)
# - Apply defined configuration to endpoint
import asyncio
import base64
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
@@ -82,22 +83,18 @@ import confluent.log as log
import confluent.messages as msg
import confluent.networking.macmap as macmap
import confluent.noderange as noderange
import confluent.tasks as tasks
import confluent.util as util
import inspect
import json
import eventlet
import traceback
import shlex
import struct
import eventlet.green.socket as socket
import socket
import socket as nsocket
import eventlet.green.subprocess as subprocess
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
import eventlet
import eventlet.greenpool
import eventlet.semaphore
autosensors = set()
scanner = None
@@ -106,6 +103,11 @@ try:
except NameError:
unicode = str
try:
import cryptography.x509.verification as verification
except ImportError:
verification = None
class nesteddict(dict):
def __missing__(self, key):
@@ -165,7 +167,6 @@ servicebyname = {
'lenovo-tsm': 'service:lenovo-tsm',
}
discopool = eventlet.greenpool.GreenPool(500)
runningevals = {}
# Passive-only auto-detection protocols:
# PXE
@@ -526,13 +527,13 @@ def save_subscriptions(subs):
dso.write(json.dumps(subs))
def register_remote_addrs(addresses, configmanager):
def register_remote_addr(addr):
async def register_remote_addrs(addresses, configmanager):
async def register_remote_addr(addr):
nd = {
'addresses': [(addr, 443)]
}
try:
sd = ssdp.check_fish(('/DeviceDescription.json', nd))
sd = await ssdp.check_fish(('/DeviceDescription.json', nd))
if not sd:
return addr, False
if 'macaddress' in sd['attributes']:
@@ -543,22 +544,21 @@ def register_remote_addrs(addresses, configmanager):
nh = xcc3.NodeHandler(sd, configmanager)
elif 'lenovo-xcc' in sd['services']:
nh = xcc.NodeHandler(sd, configmanager)
nh.scan()
detected(nh.info)
await nh.scan()
await detected(nh.info)
except Exception:
return addr, False
return addr, True
rpool = eventlet.greenpool.GreenPool(512)
for count in iterate_addrs(addresses, True):
yield msg.ConfluentResourceCount(count)
for result in rpool.imap(register_remote_addr, iterate_addrs(addresses)):
async for result in tasks.task_imap(register_remote_addr, iterate_addrs(addresses), max_concurrent=512):
if result[1]:
yield msg.CreatedResource(result[0])
else:
yield msg.ConfluentResourceNotFound(result[0])
def handle_api_request(configmanager, inputdata, operation, pathcomponents):
async def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if pathcomponents == ['discovery', 'autosense']:
return handle_autosense_config(operation, inputdata)
if operation == 'retrieve' and pathcomponents[:2] == ['discovery', 'subscriptions']:
@@ -572,7 +572,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
pathcomponents == ['discovery', 'rescan']):
if inputdata != {'rescan': 'start'}:
raise exc.InvalidArgumentException()
rescan()
await rescan()
return (msg.KeyValueData({'rescan': 'started'}),)
elif operation in ('update', 'create') and pathcomponents[:2] == ['discovery', 'subscriptions']:
target = pathcomponents[2]
@@ -593,7 +593,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if pathcomponents == ['discovery', 'register']:
if 'addresses' not in inputdata:
raise exc.InvalidArgumentException('Missing address in input')
return register_remote_addrs(inputdata['addresses'], configmanager)
return await register_remote_addrs(inputdata['addresses'], configmanager)
if 'node' not in inputdata:
raise exc.InvalidArgumentException('Missing node name in input')
mac = _get_mac_from_query(pathcomponents)
@@ -604,7 +604,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
'/'.join(pathcomponents)))
handler = info['handler'].NodeHandler(info, configmanager)
try:
eval_node(configmanager, handler, info, inputdata['node'],
await eval_node(configmanager, handler, info, inputdata['node'],
manual=True)
except Exception as e:
# or... incorrect passworod provided..
@@ -674,17 +674,17 @@ def detected_models():
yield info['modelnumber']
def _recheck_nodes(nodeattribs, configmanager):
async def _recheck_nodes(nodeattribs, configmanager):
if not cfm.config_is_ready():
return
if rechecklock.locked():
# if already in progress, don't run again
# it may make sense to schedule a repeat, but will try the easier and less redundant way first
return
with rechecklock:
return _recheck_nodes_backend(nodeattribs, configmanager)
async with rechecklock:
return await _recheck_nodes_backend(nodeattribs, configmanager)
def _recheck_nodes_backend(nodeattribs, configmanager):
async def _recheck_nodes_backend(nodeattribs, configmanager):
global rechecker
_map_unique_ids(nodeattribs)
# for the nodes whose attributes have changed, consider them as potential
@@ -700,7 +700,7 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
# Now we go through ones we did not find earlier
for mac in list(unknown_info):
try:
_recheck_single_unknown(configmanager, mac)
await _recheck_single_unknown(configmanager, mac)
except Exception:
traceback.print_exc()
continue
@@ -712,19 +712,19 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
if info['handler'] is None:
next
handler = info['handler'].NodeHandler(info, configmanager)
discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
tasks.spawn(eval_node(configmanager, handler, info, nodename))
except Exception:
traceback.print_exc()
log.log({'error': 'Unexpected error during discovery of {0}, check debug '
'logs'.format(nodename)})
def _recheck_single_unknown(configmanager, mac):
async def _recheck_single_unknown(configmanager, mac):
info = unknown_info.get(mac, None)
_recheck_single_unknown_info(configmanager, info)
await _recheck_single_unknown_info(configmanager, info)
def _recheck_single_unknown_info(configmanager, info):
async def _recheck_single_unknown_info(configmanager, info):
global rechecker
global rechecktime
if not info or info['handler'] is None:
@@ -755,12 +755,12 @@ def _recheck_single_unknown_info(configmanager, info):
if rechecker is not None and rechecktime > util.monotonic_time() + 300:
rechecker.cancel()
# if cancel did not result in dead, then we are in progress
if rechecker is None or rechecker.dead:
if rechecker is None or rechecker.done():
rechecktime = util.monotonic_time() + 300
rechecker = eventlet.spawn_after(300, _periodic_recheck,
rechecker = tasks.spawn_task_after(300, _periodic_recheck,
configmanager)
return
nodename, info['maccount'] = get_nodename(configmanager, handler, info)
nodename, info['maccount'] = await get_nodename(configmanager, handler, info)
if nodename:
if handler.https_supported:
dp = configmanager.get_node_attributes([nodename],
@@ -772,7 +772,7 @@ def _recheck_single_unknown_info(configmanager, info):
known_nodes[nodename][info['hwaddr']] = info
info['discostatus'] = 'discovered'
return # already known, no need for more
discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
tasks.spawn(eval_node(configmanager, handler, info, nodename))
def safe_detected(info):
@@ -781,18 +781,18 @@ def safe_detected(info):
if info['hwaddr'] in runningevals:
# Do not evaluate the same mac multiple times at once
return
runningevals[info['hwaddr']] = discopool.spawn(eval_detected, info)
runningevals[info['hwaddr']] = tasks.spawn_task(eval_detected(info))
def eval_detected(info):
async def eval_detected(info):
try:
detected(info)
await detected(info)
except Exception as e:
traceback.print_exc()
del runningevals[info['hwaddr']]
def detected(info):
async def detected(info):
global rechecker
global rechecktime
if not cfm.config_is_ready():
@@ -810,7 +810,7 @@ def detected(info):
return
if (handler and not handler.NodeHandler.adequate(info) and
info.get('protocol', None)):
eventlet.spawn_after(10, info['protocol'].fix_info, info,
tasks.spawn_after(10, info['protocol'].fix_info, info,
safe_detected)
return
if info['hwaddr'] in known_info and 'addresses' in info:
@@ -843,7 +843,9 @@ def detected(info):
cfg = cfm.ConfigManager(None)
if handler:
handler = handler.NodeHandler(info, cfg)
handler.scan()
res = handler.scan()
if inspect.isawaitable(res):
await res
try:
if 'modelnumber' not in info:
info['modelnumber'] = info['attributes']['enclosure-machinetype-model'][0]
@@ -885,15 +887,15 @@ def detected(info):
)})
if rechecker is not None and rechecktime > util.monotonic_time() + 300:
rechecker.cancel()
if rechecker is None or rechecker.dead:
if rechecker is None or rechecker.done():
rechecktime = util.monotonic_time() + 300
rechecker = eventlet.spawn_after(300, _periodic_recheck, cfg)
rechecker = tasks.spawn_task_after(300, _periodic_recheck, cfg)
unknown_info[info['hwaddr']] = info
info['discostatus'] = 'unidentfied'
#TODO, eventlet spawn after to recheck sooner, or somehow else
#TODO, spawn after to recheck sooner, or somehow else
# influence periodic recheck to shorten delay?
return
nodename, info['maccount'] = get_nodename(cfg, handler, info)
nodename, info['maccount'] = await get_nodename(cfg, handler, info)
if nodename and handler and handler.https_supported:
dp = cfg.get_node_attributes([nodename],
('pubkeys.tls_hardwaremanager', 'id.uuid', 'discovery.policy'))
@@ -920,7 +922,7 @@ def detected(info):
#for now defer probe until inside eval_node. We might not have
#a nodename without probe in the future.
if nodename and handler:
eval_node(cfg, handler, info, nodename)
await eval_node(cfg, handler, info, nodename)
elif handler:
#log.log(
# {'info': 'Detected unknown {0} with hwaddr {1} at '
@@ -959,7 +961,7 @@ def get_enclosure_chain_head(nodename, cfg):
return nodename
def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
async def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
# nodename is the head of the chain, cfg is a configmanager, handler
# is the handler of the current candidate, nl is optional indication
# of the next link in the chain, checkswitch can disable the switch
@@ -995,7 +997,7 @@ def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
if pkey:
cv = util.TLSCertVerifier(
cfg, nodename, 'pubkeys.tls_hardwaremanager').verify_cert
for fprint in get_smm_neighbor_fingerprints(smmaddr, cv):
async for fprint in get_smm_neighbor_fingerprints(smmaddr, cv):
if util.cert_matches(fprint, mycert):
# a trusted chain member vouched for the cert
# so it's validated
@@ -1007,12 +1009,12 @@ def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
return None, False
def get_smm_neighbor_fingerprints(smmaddr, cv):
async def get_smm_neighbor_fingerprints(smmaddr, cv):
if ':' in smmaddr:
smmaddr = '[{0}]'.format(smmaddr)
wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv)
wc = webclient.WebConnection(smmaddr, verifycallback=cv)
try:
neighs = wc.grab_json_response('/scripts/neighdata.json')
neighs = await wc.grab_json_response('/scripts/neighdata.json')
except Exception:
log.log({'error': 'Failure getting LLDP information from {}'.format(smmaddr)})
return
@@ -1059,13 +1061,17 @@ 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]
def get_nodename(cfg, handler, info):
async def get_nodename(cfg, handler, info):
nodename = None
maccount = None
info['verified'] = False
@@ -1107,7 +1113,7 @@ def get_nodename(cfg, handler, info):
if not nodename: # as a last resort, search switches for info
# This is the slowest potential operation, so we hope for the
# best to occur prior to this
nodename, macinfo = macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg)
nodename, macinfo = await macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg)
maccount = macinfo['maccount']
if nodename:
if handler.devname in ('SMM', 'SMM3'):
@@ -1182,18 +1188,25 @@ 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
def search_smms_by_cert(currsmm, cert, cfg):
async def search_smms_by_cert(currsmm, cert, cfg):
neighs = []
cv = util.TLSCertVerifier(
cfg, currsmm, 'pubkeys.tls_hardwaremanager').verify_cert
@@ -1203,8 +1216,8 @@ def search_smms_by_cert(currsmm, cert, cfg):
smmaddr = cd.get(currsmm, {}).get('hardwaremanagement.manager', {}).get('value', None)
if not smmaddr:
smmaddr = currsmm
wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv)
neighs = wc.grab_json_response('/scripts/neighdata.json')
wc = webclient.WebConnection(smmaddr, verifycallback=cv)
neighs = await wc.grab_json_response('/scripts/neighdata.json')
except Exception:
return None
for neigh in neighs:
@@ -1215,24 +1228,29 @@ 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
exnl = list(cfg.filter_node_attributes('enclosure.extends=' + currsmm))
if len(exnl) == 1:
return search_smms_by_cert(exnl[0], cert, cfg)
return await search_smms_by_cert(exnl[0], cert, cfg)
def eval_node(cfg, handler, info, nodename, manual=False):
async def eval_node(cfg, handler, info, nodename, manual=False):
try:
handler.probe() # unicast interrogation as possible to get more data
# switch concurrently
# do some preconfig, for example, to bring a SMM online if applicable
handler.preconfig(nodename)
await handler.preconfig(nodename)
except Exception as e:
unknown_info[info['hwaddr']] = info
info['discostatus'] = 'unidentified'
@@ -1261,12 +1279,12 @@ def eval_node(cfg, handler, info, nodename, manual=False):
# The specified node is an enclosure (has nodes mapped to it), but
# what we are talking to is *not* an enclosure
# might be ambiguous, need to match chassis-uuid as well..
match = search_smms_by_cert(nodename, handler.https_cert, cfg)
match = await search_smms_by_cert(nodename, handler.https_cert, cfg)
if match:
info['verfied'] = True
info['enclosure.bay'] = match[1]
if match[2]:
if not discover_node(cfg, handler, info, match[2], manual):
if not await discover_node(cfg, handler, info, match[2], manual):
pending_nodes[match[2]] = info
return
if 'enclosure.bay' not in info:
@@ -1311,8 +1329,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 +1348,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:
@@ -1333,7 +1358,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
info['discostatus'] = 'unidentified'
return
nodename = nl[0]
if not discover_node(cfg, handler, info, nodename, manual):
if not await discover_node(cfg, handler, info, nodename, manual):
# store it as pending, assuming blocked on enclosure
# assurance...
pending_nodes[nodename] = info
@@ -1353,7 +1378,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
fprints = macmap.get_node_fingerprints(nodename, cfg)
for fprint in fprints:
if util.cert_matches(fprint[0], handler.https_cert):
if not discover_node(cfg, handler, info,
if not await discover_node(cfg, handler, info,
nodename, manual):
pending_nodes[nodename] = info
return
@@ -1366,11 +1391,11 @@ def eval_node(cfg, handler, info, nodename, manual=False):
'defined nodes for the enclosure'.format(nodename, handler.devname)
log.log({'error': errorstr})
return
if not discover_node(cfg, handler, info, nodename, manual):
if not await discover_node(cfg, handler, info, nodename, manual):
pending_nodes[nodename] = info
def discover_node(cfg, handler, info, nodename, manual):
async def discover_node(cfg, handler, info, nodename, manual):
if manual:
if not cfg.is_node(nodename):
raise exc.InvalidArgumentException(
@@ -1430,7 +1455,7 @@ def discover_node(cfg, handler, info, nodename, manual):
elif manual or not util.cert_matches(lastfp, handler.https_cert):
# only 'discover' if it is not the same as last time
try:
handler.config(nodename)
await handler.config(nodename)
except Exception as e:
info['discofailure'] = 'bug'
if manual:
@@ -1468,26 +1493,29 @@ def discover_node(cfg, handler, info, nodename, manual):
for checkattr in newnodeattribs:
checkval = currattrs.get(nodename, {}).get(checkattr, {}).get('value', None)
if checkval != newnodeattribs[checkattr]:
cfg.set_node_attributes({nodename: newnodeattribs})
await cfg.set_node_attributes({nodename: newnodeattribs})
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:
log.log({'error': 'Unable to get BMC address for {0]'.format(nodename)})
else:
bmcaddr = bmcaddr.split('/', 1)[0]
wait_for_connection(bmcaddr)
await wait_for_connection(bmcaddr)
socket.getaddrinfo(bmcaddr, 443)
subprocess.check_call(['/opt/confluent/bin/nodeconfig', nodename] + nodeconfig)
if nodeconfig:
await util.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():
await handler.autosign_certificate()
info['discostatus'] = 'discovered'
for i in pending_by_uuid.get(curruuid, []):
eventlet.spawn_n(_recheck_single_unknown_info, cfg, i)
tasks.spawn(_recheck_single_unknown_info(cfg, i))
try:
del pending_by_uuid[curruuid]
except KeyError:
@@ -1508,10 +1536,11 @@ def discover_node(cfg, handler, info, nodename, manual):
info['discofailure'] = 'policy'
return False
def wait_for_connection(bmcaddr):
async def wait_for_connection(bmcaddr):
cloop = asyncio.get_running_loop()
expiry = 75 + util.monotonic_time()
while util.monotonic_time() < expiry:
for addrinf in socket.getaddrinfo(bmcaddr, 443, proto=socket.IPPROTO_TCP):
for addrinf in await cloop.getaddrinfo(bmcaddr, 443, proto=socket.IPPROTO_TCP):
try:
tsock = socket.socket(addrinf[0])
tsock.settimeout(1)
@@ -1519,7 +1548,7 @@ def wait_for_connection(bmcaddr):
return
except OSError:
continue
eventlet.sleep(1)
await asyncio.sleep(1)
def do_pxe_discovery(cfg, handler, info, manual, nodename, policies):
# use uuid based scheme in lieu of tls cert, ideally only
@@ -1568,19 +1597,19 @@ nodeaddhandler = None
needaddhandled = False
def _handle_nodelist_change(configmanager):
async def _handle_nodelist_change(configmanager):
global needaddhandled
global nodeaddhandler
macmap.vintage = 0 # the current mac map is probably inaccurate
_recheck_nodes((), configmanager)
await _recheck_nodes((), configmanager)
if needaddhandled:
needaddhandled = False
nodeaddhandler = eventlet.spawn(_handle_nodelist_change, configmanager)
nodeaddhandler = tasks.spawn_task(_handle_nodelist_change(configmanager))
else:
nodeaddhandler = None
def newnodes(added, deleting, renamed, configmanager):
async def newnodes(added, deleting, renamed, configmanager):
global attribwatcher
global needaddhandled
global nodeaddhandler
@@ -1603,20 +1632,20 @@ def newnodes(added, deleting, renamed, configmanager):
if nodeaddhandler:
needaddhandled = True
else:
nodeaddhandler = eventlet.spawn(_handle_nodelist_change, configmanager)
nodeaddhandler = tasks.spawn_task(_handle_nodelist_change(configmanager))
rechecker = None
rechecktime = None
rechecklock = eventlet.semaphore.Semaphore()
rechecklock = asyncio.Lock()
def _periodic_recheck(configmanager):
async def _periodic_recheck(configmanager):
global rechecker
global rechecktime
rechecker = None
try:
_recheck_nodes((), configmanager)
await _recheck_nodes((), configmanager)
except Exception:
traceback.print_exc()
log.log({'error': 'Unexpected error during discovery, check debug '
@@ -1625,35 +1654,36 @@ def _periodic_recheck(configmanager):
# for rechecker was requested in the course of recheck_nodes
if rechecker is None:
rechecktime = util.monotonic_time() + 900
rechecker = eventlet.spawn_after(900, _periodic_recheck,
rechecker = tasks.spawn_task_after(900, _periodic_recheck,
configmanager)
def rescan():
async def rescan():
_map_unique_ids()
global scanner
if scanner:
return
else:
scanner = eventlet.spawn(blocking_scan)
remotescan()
scanner = tasks.spawn_task(blocking_scan())
await remotescan()
def remotescan():
async def remotescan():
mycfm = cfm.ConfigManager(None)
myname = collective.get_myname()
for remagent in get_subscriptions():
try:
affluent.renotify_me(remagent, mycfm, myname)
await affluent.renotify_me(remagent, mycfm, myname)
except Exception as e:
log.log({'error': 'Unexpected problem asking {} for discovery notifications'.format(remagent)})
def blocking_scan():
async def blocking_scan():
global scanner
slpscan = eventlet.spawn(slp.active_scan, safe_detected, slp)
ssdpscan = eventlet.spawn(ssdp.active_scan, safe_detected, ssdp)
slpscan.wait()
ssdpscan.wait()
slpscan = tasks.spawn_task(slp.active_scan(safe_detected, slp))
ssdpscan = tasks.spawn_task(ssdp.active_scan(safe_detected, ssdp))
await slpscan
await ssdpscan
#ssdpscan.wait()
scanner = None
def start_detection():
@@ -1673,19 +1703,18 @@ def start_detection():
start_autosense()
if rechecker is None:
rechecktime = util.monotonic_time() + 900
rechecker = eventlet.spawn_after(900, _periodic_recheck, cfg)
eventlet.spawn_n(ssdp.snoop, safe_detected, None, ssdp, get_node_by_uuid_or_mac)
rechecker = tasks.spawn_task_after(900, _periodic_recheck, cfg)
tasks.spawn(ssdp.snoop(safe_detected, None, ssdp, get_node_by_uuid_or_mac))
def stop_autosense():
for watcher in list(autosensors):
watcher.kill()
watcher.cancel()
autosensors.discard(watcher)
def start_autosense():
autosensors.add(eventlet.spawn(slp.snoop, safe_detected, slp))
#autosensors.add(eventlet.spawn(mdns.snoop, safe_detected, mdns))
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected, pxe, get_node_guess_by_uuid))
eventlet.spawn(remotescan)
autosensors.add(tasks.spawn_task(slp.snoop(safe_detected, slp)))
tasks.spawn(pxe.snoop(safe_detected, pxe, get_node_guess_by_uuid))
tasks.spawn(remotescan())
nodes_by_fprint = {}
@@ -1729,8 +1758,10 @@ def _map_unique_ids(nodes=None):
nodes_by_fprint[fprint] = node
if __name__ == '__main__':
async def main():
start_detection()
while True:
eventlet.sleep(30)
await asyncio.sleep(30)
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())
@@ -15,24 +15,19 @@
import confluent.discovery.handlers.generic as generic
import confluent.exceptions as exc
import confluent.netutil as netutil
import eventlet.support.greendns
# Provide foundation for general IPMI device configuration
import pyghmi.exceptions as pygexc
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
ipmicommand.session.select = eventlet.green.select
ipmicommand.session.threading = eventlet.green.threading
ipmicommand.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
getaddrinfo = eventlet.support.greendns.getaddrinfo
import aiohmi.exceptions as pygexc
import aiohmi.ipmi.command as ipmicommand
import socket
class NodeHandler(generic.NodeHandler):
DEFAULT_USER = 'USERID'
DEFAULT_PASS = 'PASSW0RD'
def _get_ipmicmd(self, user=None, password=None):
async def _get_ipmicmd(self, user=None, password=None):
priv = None
if user is None or password is None:
if self.trieddefault:
@@ -42,7 +37,7 @@ class NodeHandler(generic.NodeHandler):
user = self.DEFAULT_USER
if password is None:
password = self.DEFAULT_PASS
return ipmicommand.Command(self.ipaddr, user, password,
return await ipmicommand.create(self.ipaddr, user, password,
privlevel=priv, keepalive=False)
def __init__(self, info, configmanager):
@@ -56,7 +51,7 @@ class NodeHandler(generic.NodeHandler):
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(self, nodename, reset=False, customconfig=None, vc=None):
async def _bmcconfig(self, nodename, reset=False, customconfig=None, vc=None):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
@@ -69,7 +64,7 @@ class NodeHandler(generic.NodeHandler):
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', None)
try:
ic = self._get_ipmicmd()
ic = await self._get_ipmicmd()
passwd = self.DEFAULT_PASS
except pygexc.IpmiException as pi:
havecustomcreds = False
@@ -82,14 +77,14 @@ class NodeHandler(generic.NodeHandler):
else:
passwd = self.DEFAULT_PASS
if havecustomcreds:
ic = self._get_ipmicmd(user, passwd)
ic = await self._get_ipmicmd(user, passwd)
else:
raise
if vc:
ic.register_key_handler(vc)
currusers = ic.get_users()
lanchan = ic.get_network_channel()
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
currusers = await ic.get_users()
lanchan = await ic.get_network_channel()
userdata = await ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
1))
userdata = bytearray(userdata['data'])
maxusers = userdata[0] & 0b111111
@@ -114,7 +109,7 @@ class NodeHandler(generic.NodeHandler):
newuserslot = uid
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
ic = self._get_ipmicmd(user, passwd)
ic = await self._get_ipmicmd(user, passwd)
if vc:
ic.register_key_handler(vc)
break
@@ -126,7 +121,7 @@ class NodeHandler(generic.NodeHandler):
ic.set_user_password(newuserslot, password=newpass)
ic.set_user_name(newuserslot, newuser)
if havecustomcreds:
ic = self._get_ipmicmd(user, passwd)
ic = await self._get_ipmicmd(user, passwd)
if vc:
ic.register_key_handler(vc)
#We are remote operating on the account we are
@@ -161,13 +156,13 @@ class NodeHandler(generic.NodeHandler):
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newip = newip.split('/', 1)[0]
newipinfo = getaddrinfo(newip, 0)[0]
newipinfo = socket.getaddrinfo(newip, 0)[0]
# This getaddrinfo is repeated in get_nic_config, could be
# optimized, albeit with a more convoluted api..
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
netconfig = await netutil.get_nic_config(cfg, nodename, ip=newip)
plen = netconfig['prefix']
newip = '{0}/{1}'.format(newip, plen)
currcfg = ic.get_net_configuration()
@@ -13,13 +13,13 @@
# limitations under the License.
import confluent.discovery.handlers.bmc as bmchandler
import eventlet
import confluent.util as util
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
class NodeHandler(bmchandler.NodeHandler):
DEFAULT_USER = 'admin'
@@ -33,13 +33,13 @@ class NodeHandler(bmchandler.NodeHandler):
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def get_webclient(self, user, passwd, newuser, newpass):
wc = webclient.SecureHTTPConnection(self.ipaddr, 443,
async def get_webclient(self, user, passwd, newuser, newpass):
wc = webclient.WebConnection(self.ipaddr, 443,
verifycallback=self.validate_cert)
wc.connect()
await wc.connect()
authdata = urlencode({'username': user, 'password': passwd,
'weblogsign': 1})
res = wc.grab_json_response_with_status('/api/session', authdata)
res = await wc.grab_json_response_with_status('/api/session', authdata)
if res[1] == 200:
if res[0].get('force_password', 1) == 0:
# Need to handle password change
@@ -51,12 +51,12 @@ class NodeHandler(bmchandler.NodeHandler):
'privilege': 4,
}
passchange = urlencode(passchange)
rsp = wc.grab_json_response_with_status('/api/reset-pass',
rsp = await wc.grab_json_response_with_status('/api/reset-pass',
passchange)
rsp = wc.grab_json_response_with_status('/api/session',
rsp = await wc.grab_json_response_with_status('/api/session',
method='DELETE')
def config(self, nodename, reset=False):
async def config(self, nodename, reset=False):
self.nodename = nodename
creds = self.configmanager.get_node_attributes(
self.nodename, ['secret.hardwaremanagementuser',
@@ -65,6 +65,6 @@ class NodeHandler(bmchandler.NodeHandler):
user, passwd, isdefault = self.get_node_credentials(
nodename, creds, 'admin', 'admin')
if not isdefault:
self.get_webclient(self.DEFAULT_USER, self.DEFAULT_PASS, user,
await self.get_webclient(self.DEFAULT_USER, self.DEFAULT_PASS, user,
passwd)
self._bmcconfig(nodename, False)
await self._bmcconfig(nodename, False)
@@ -14,9 +14,12 @@
import confluent.util as util
import errno
import eventlet
import socket
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
try:
import cryptography.x509 as x509
except ImportError:
x509 = None
class NodeHandler(object):
https_supported = True
@@ -37,6 +40,7 @@ class NodeHandler(object):
self.relay_server = None
self.web_ip = None
self.web_port = None
self.https_cert = None
# if this is a remote registered component, prefer to use the agent forwarder
if info.get('forwarder_url', False):
self.relay_url = info['forwarder_url']
@@ -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.WebConnection(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
async 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
@@ -70,7 +108,7 @@ class NodeHandler(object):
# serial number and uuid to flesh out data as needed
return
def preconfig(self, possiblenode):
async def preconfig(self, possiblenode):
return
def discoverable_by_switch(self, macs):
@@ -114,14 +152,13 @@ class NodeHandler(object):
elif self._certfailreason == 2:
return 'unreachable'
@property
def https_cert(self):
async def get_https_cert(self):
if self._fp:
return self._fp
ip, port = self.get_web_port_and_ip()
wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert, port=port)
ip, port = await self.get_web_port_and_ip()
wc = webclient.WebConnection(ip, verifycallback=self._savecert, port=port)
try:
wc.connect()
await wc.request('GET', '/')
except IOError as ie:
if ie.errno == errno.ECONNREFUSED:
self._certfailreason = 1
@@ -134,16 +171,17 @@ class NodeHandler(object):
except Exception:
self._certfailreason = 2
return None
self.https_cert = self._fp
return self._fp
def get_web_port_and_ip(self):
async def get_web_port_and_ip(self):
if self.web_ip:
return self.web_ip, self.web_port
# get target ip and port, either direct or relay as applicable
if self.relay_url:
kv = util.TLSCertVerifier(self.configmanager, self.relay_server,
'pubkeys.tls_hardwaremanager').verify_cert
w = webclient.SecureHTTPConnection(self.relay_server, verifycallback=kv)
w = webclient.WebConnection(self.relay_server, verifycallback=kv)
relaycreds = self.configmanager.get_node_attributes(self.relay_server, 'secret.*', decrypt=True)
relaycreds = relaycreds.get(self.relay_server, {})
relayuser = relaycreds.get('secret.hardwaremanagementuser', {}).get('value', None)
@@ -151,8 +189,7 @@ class NodeHandler(object):
if not relayuser or not relaypass:
raise Exception('No credentials for {0}'.format(self.relay_server))
w.set_basic_credentials(relayuser, relaypass)
w.connect()
w.request('GET', self.relay_url)
await w.request('GET', self.relay_url)
r = w.getresponse()
rb = r.read()
if r.code != 302:
@@ -14,8 +14,8 @@
import codecs
import confluent.discovery.handlers.bmc as bmchandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.private.util as pygutil
import aiohmi.exceptions as pygexc
import aiohmi.ipmi.private.util as pygutil
import confluent.util as util
import struct
@@ -73,7 +73,7 @@ class NodeHandler(bmchandler.NodeHandler):
if slot != 0:
self.info['enclosure.bay'] = slot
def probe(self):
async def probe(self):
if self.info.get('enclosure.bay', 0) == 0:
self.scan()
if self.info.get('enclosure.bay', 0) != 0:
@@ -85,8 +85,8 @@ class NodeHandler(bmchandler.NodeHandler):
try:
# we are a dense platform, but the SLP data did not give us slot
# attempt to probe using IPMI
ipmicmd = self._get_ipmicmd()
guiddata = ipmicmd.xraw_command(netfn=6, command=8)
ipmicmd = await self._get_ipmicmd()
guiddata = await ipmicmd.xraw_command(netfn=6, command=8)
self.info['uuid'] = pygutil.decode_wireformat_uuid(
guiddata['data']).lower()
ipmicmd.oem_init()
@@ -12,12 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.discovery.handlers.redfishbmc as redfishbmc
import eventlet.support.greendns
getaddrinfo = eventlet.support.greendns.getaddrinfo
class NodeHandler(redfishbmc.NodeHandler):
@@ -25,19 +22,19 @@ class NodeHandler(redfishbmc.NodeHandler):
return ('admin', 'admin')
def remote_nodecfg(nodename, cfm):
async def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
nodename, 'hardwaremanagement.manager')
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
'value', None)
ipaddr = ipaddr.split('/', 1)[0]
ipaddr = getaddrinfo(ipaddr, 0)[0][-1]
ipaddr = (await asyncio.get_event_loop().getaddrinfo(ipaddr, 0))[0][-1]
if not ipaddr:
raise Exception('Cannot remote configure a system without known '
'address')
info = {'addresses': [ipaddr]}
nh = NodeHandler(info, cfm)
nh.config(nodename)
await nh.config(nodename)
if __name__ == '__main__':
@@ -12,32 +12,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.discovery.handlers.generic as generic
import confluent.exceptions as exc
import confluent.netutil as netutil
import confluent.util as util
import eventlet
import eventlet.support.greendns
import json
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
getaddrinfo = eventlet.support.greendns.getaddrinfo
webclient = eventlet.import_patched('pyghmi.util.webclient')
from socket import getaddrinfo
def get_host_interface_urls(wc, mginfo):
import aiohmi.util.webclient as webclient
async def get_host_interface_urls(wc, mginfo):
returls = []
hifurl = mginfo.get('HostInterfaces', {}).get('@odata.id', None)
if not hifurl:
return []
hifinfo = wc.grab_json_response(hifurl)
hifinfo = await wc.grab_json_response(hifurl)
hifurls = hifinfo.get('Members', [])
for hifurl in hifurls:
hifurl = hifurl['@odata.id']
hifinfo = wc.grab_json_response(hifurl)
hifinfo = await wc.grab_json_response(hifurl)
acturl = hifinfo.get('ManagerEthernetInterface', {}).get('@odata.id', None)
if acturl:
returls.append(acturl)
@@ -61,35 +57,36 @@ class NodeHandler(generic.NodeHandler):
self._mgrinfo = None
super(NodeHandler, self).__init__(info, configmanager)
def srvroot(self, wc):
async def srvroot(self, wc):
if not self._srvroot:
srvroot, status = wc.grab_json_response_with_status('/redfish/v1/')
srvroot, status = await wc.grab_json_response_with_status('/redfish/v1/')
if status == 200:
self._srvroot = srvroot
return self._srvroot
def get_manager_url(self, wc):
mgrs = self.srvroot(wc).get('Managers', {}).get('@odata.id', None)
async def get_manager_url(self, wc):
mgrs = (await self.srvroot(wc)).get('Managers', {}).get('@odata.id', None)
if not mgrs:
raise Exception("No Managers resource on BMC")
rsp = wc.grab_json_response(mgrs)
rsp = await wc.grab_json_response(mgrs)
if len(rsp.get('Members', [])) != 1:
raise Exception("Can not handle multiple Managers")
mgrurl = rsp['Members'][0]['@odata.id']
return mgrurl
def mgrinfo(self, wc):
async def mgrinfo(self, wc):
if not self._mgrinfo:
self._mgrinfo = wc.grab_json_response(self.get_manager_url(wc))
self._mgrinfo = await wc.grab_json_response(await self.get_manager_url(wc))
return self._mgrinfo
def get_firmware_default_account_info(self):
raise Exception('This must be subclassed')
def scan(self):
c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
i = c.grab_json_response('/redfish/v1/')
async def scan(self):
await self.get_https_cert()
c = webclient.WebConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
i = await c.grab_json_response('/redfish/v1/')
uuid = i.get('UUID', None)
if uuid:
self.info['uuid'] = uuid.lower()
@@ -100,18 +97,19 @@ class NodeHandler(generic.NodeHandler):
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def enable_ipmi(self, wc):
npu = self.mgrinfo(wc).get(
async def enable_ipmi(self, wc):
mgrinfo = await self.mgrinfo(wc)
npu =mgrinfo.get(
'NetworkProtocol', {}).get('@odata.id', None)
if not npu:
raise Exception('Cannot enable IPMI, no NetworkProtocol on BMC')
npi = wc.grab_json_response(npu)
npi = await wc.grab_json_response(npu)
if not npi.get('IPMI', {}).get('ProtocolEnabled'):
wc.set_header('If-Match', '*')
wc.grab_json_response_with_status(
await wc.grab_json_response_with_status(
npu, {'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
acctinfo = wc.grab_json_response_with_status(
self.target_account_url(wc))
acctinfo = await wc.grab_json_response_with_status(
await self.target_account_url(wc))
acctinfo = acctinfo[0]
actypes = acctinfo['AccountTypes']
candidates = acctinfo.get('AccountTypes@Redfish.AllowableValues', [])
@@ -121,18 +119,19 @@ class NodeHandler(generic.NodeHandler):
'AccountTypes': actypes,
'Password': self.currpass,
}
rsp = wc.grab_json_response_with_status(
self.target_account_url(wc), acctupd, method='PATCH')
rsp = await wc.grab_json_response_with_status(
await self.target_account_url(wc), acctupd, method='PATCH')
def _get_wc(self):
async def _get_wc(self):
await self.get_https_cert()
defuser, defpass = self.get_firmware_default_account_info()
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
wc = webclient.WebConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
wc.set_basic_credentials(defuser, defpass)
wc.set_header('Content-Type', 'application/json')
wc.set_header('Accept', 'application/json')
authmode = 0
if not self.trieddefault:
rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
if status == 403:
self.trieddefault = True
chgurl = None
@@ -156,30 +155,30 @@ class NodeHandler(generic.NodeHandler):
if self.targpass == defpass:
raise Exception("Must specify a non-default password to onboard this BMC")
wc.set_header('If-Match', '*')
cpr = wc.grab_json_response_with_status(chgurl, {'Password': self.targpass}, method='PATCH')
cpr = await wc.grab_json_response_with_status(chgurl, {'Password': self.targpass}, method='PATCH')
if cpr[1] >= 200 and cpr[1] < 300:
self.curruser = defuser
self.currpass = self.targpass
wc.set_basic_credentials(self.curruser, self.currpass)
_, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
_, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
tries = 10
while status >= 300 and tries:
eventlet.sleep(1)
_, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
await asyncio.sleep(1)
_, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
return wc
if status > 400:
self.trieddefault = True
if status == 401:
wc.set_basic_credentials(defuser, self.targpass)
rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
if status == 200: # Default user still, but targpass
self.currpass = self.targpass
self.curruser = defuser
return wc
elif self.targuser != defuser:
wc.set_basic_credentials(self.targuser, self.targpass)
rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
if status != 200:
raise Exception("Target BMC does not recognize firmware default credentials nor the confluent stored credential")
else:
@@ -188,28 +187,29 @@ class NodeHandler(generic.NodeHandler):
return wc
if self.curruser:
wc.set_basic_credentials(self.curruser, self.currpass)
rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
if status != 200:
return None
return wc
wc.set_basic_credentials(self.targuser, self.targpass)
rsp, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
if status != 200:
return None
self.curruser = self.targuser
self.currpass = self.targpass
return wc
def target_account_url(self, wc):
asrv = self.srvroot(wc).get('AccountService', {}).get('@odata.id')
rsp, status = wc.grab_json_response_with_status(asrv)
async def target_account_url(self, wc):
srvroot = await self.srvroot(wc)
asrv = srvroot.get('AccountService', {}).get('@odata.id')
rsp, status = await wc.grab_json_response_with_status(asrv)
accts = rsp.get('Accounts', {}).get('@odata.id')
rsp, status = wc.grab_json_response_with_status(accts)
rsp, status = await wc.grab_json_response_with_status(accts)
accts = rsp.get('Members', [])
for accturl in accts:
accturl = accturl.get('@odata.id', '')
if accturl:
rsp, status = wc.grab_json_response_with_status(accturl)
rsp, status = await wc.grab_json_response_with_status(accturl)
if rsp.get('UserName', None) == self.curruser:
targaccturl = accturl
break
@@ -217,7 +217,7 @@ class NodeHandler(generic.NodeHandler):
raise Exception("Unable to identify Account URL to modify on this BMC")
return targaccturl
def config(self, nodename):
async def config(self, nodename):
mgrs = None
self.nodename = nodename
creds = self.configmanager.get_node_attributes(
@@ -235,7 +235,7 @@ class NodeHandler(generic.NodeHandler):
passwd = util.stringify(passwd)
self.targuser = user
self.targpass = passwd
wc = self._get_wc()
wc = await self._get_wc()
curruserinfo = {}
authupdate = {}
wc.set_header('Content-Type', 'application/json')
@@ -244,23 +244,23 @@ class NodeHandler(generic.NodeHandler):
if passwd != self.currpass:
authupdate['Password'] = passwd
if authupdate:
targaccturl = self.target_account_url(wc)
rsp, status = wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH')
targaccturl = await self.target_account_url(wc)
rsp, status = await wc.grab_json_response_with_status(targaccturl, authupdate, method='PATCH')
if status >= 300:
raise Exception("Failed attempting to update credentials on BMC")
self.curruser = user
self.currpass = passwd
wc.set_basic_credentials(user, passwd)
_, status = wc.grab_json_response_with_status('/redfish/v1/Managers')
_, status = await wc.grab_json_response_with_status('/redfish/v1/Managers')
tries = 10
while tries and status >= 300:
tries -= 1
eventlet.sleep(1.0)
_, status = wc.grab_json_response_with_status(
await asyncio.sleep(1.0)
_, status = await wc.grab_json_response_with_status(
'/redfish/v1/Managers')
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
self.enable_ipmi(wc)
await self.enable_ipmi(wc)
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
@@ -271,9 +271,9 @@ class NodeHandler(generic.NodeHandler):
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
hifurls = get_host_interface_urls(wc, self.mgrinfo(wc))
hifurls = await get_host_interface_urls(wc, self.mgrinfo(wc))
mgtnicinfo = self.mgrinfo(wc)['EthernetInterfaces']['@odata.id']
mgtnicinfo = wc.grab_json_response(mgtnicinfo)
mgtnicinfo = await wc.grab_json_response(mgtnicinfo)
mgtnics = [x['@odata.id'] for x in mgtnicinfo.get('Members', [])]
actualnics = []
for candnic in mgtnics:
@@ -289,7 +289,7 @@ class NodeHandler(generic.NodeHandler):
ipkey = 'IPv4Addresses'
actualnic = None
for curractnic in actualnics:
currnicinfo = wc.grab_json_response(curractnic)
currnicinfo = await wc.grab_json_response(curractnic)
for targipaddr in currnicinfo.get(ipkey, []):
targipaddr = targipaddr.get('Address', 'Z')
if compip == targipaddr:
@@ -300,8 +300,8 @@ class NodeHandler(generic.NodeHandler):
else:
raise Exception("Unable to detect active NIC of multi-nic bmc")
actualnics = [actualnic]
currnet = wc.grab_json_response(actualnics[0])
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
currnet = await wc.grab_json_response(actualnics[0])
netconfig = await netutil.get_nic_config(self.configmanager, nodename, ip=newip)
newconfig = {
"Address": newip,
"SubnetMask": netutil.cidr_to_mask(netconfig['prefix']),
@@ -316,18 +316,29 @@ class NodeHandler(generic.NodeHandler):
break
else:
wc.set_header('If-Match', '*')
rsp, status = wc.grab_json_response_with_status(actualnics[0], {
rsp, status = await wc.grab_json_response_with_status(actualnics[0], {
'DHCPv4': {'DHCPEnabled': False},
'IPv4StaticAddresses': [newconfig]}, method='PATCH')
elif self.ipaddr.startswith('fe80::'):
self.configmanager.set_node_attributes(
await self.configmanager.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
else:
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
async 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
proc = await asyncio.create_subprocess_exec(
'/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47'
)
await proc.wait()
def remote_nodecfg(nodename, cfm):
async def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
nodename, 'hardwaremanagement.manager')
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
@@ -339,7 +350,7 @@ def remote_nodecfg(nodename, cfm):
'address')
info = {'addresses': [ipaddr]}
nh = NodeHandler(info, cfm)
nh.config(nodename)
await nh.config(nodename)
if __name__ == '__main__':
import confluent.config.configmanager as cfm
@@ -12,20 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import codecs
import confluent.discovery.handlers.bmc as bmchandler
import confluent.exceptions as exc
import eventlet
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
import struct
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import eventlet.support.greendns
import confluent.netutil as netutil
import confluent.util as util
getaddrinfo = eventlet.support.greendns.getaddrinfo
from xml.etree.ElementTree import fromstring as rfromstring
@@ -91,7 +90,7 @@ class NodeHandler(bmchandler.NodeHandler):
wc.request('POST', '/data', apirequest)
wc.getresponse().read()
def _webconfignet(self, wc, nodename):
async def _webconfignet(self, wc, nodename):
cfg = self.configmanager
if 'service:lenovo-smm2' in self.info.get('services', []):
# need to enable ipmi for now..
@@ -107,7 +106,7 @@ class NodeHandler(bmchandler.NodeHandler):
if smmip:
smmip = smmip.split('/', 1)[0]
if smmip and ':' not in smmip:
smmip = getaddrinfo(smmip, 0)[0]
smmip = await asyncio.get_event_loop().getaddrinfo(smmip, 0)[0]
smmip = smmip[-1][0]
if smmip and ':' in smmip:
raise exc.NotImplementedException('IPv6 not supported')
@@ -118,7 +117,7 @@ class NodeHandler(bmchandler.NodeHandler):
'ifConfig').find('v4IPAddr').text
if currip == smmip:
return
netconfig = netutil.get_nic_config(cfg, nodename, ip=smmip)
netconfig = await netutil.get_nic_config(cfg, nodename, ip=smmip)
netmask = netutil.cidr_to_mask(netconfig['prefix'])
setdata = 'set=ifIndex:0,v4DHCPEnabled:0,v4IPAddr:{0},v4NetMask:{1}'.format(smmip, netmask)
gateway = netconfig.get('ipv4_gateway', None)
@@ -138,7 +137,7 @@ class NodeHandler(bmchandler.NodeHandler):
def _webconfigcreds(self, username, password):
ip, port = self.get_web_port_and_ip()
wc = webclient.SecureHTTPConnection(ip, port, verifycallback=self._validate_cert)
wc = webclient.WebConnection(ip, port, verifycallback=self._validate_cert)
wc.connect()
authdata = { # start by trying factory defaults
'user': 'USERID',
@@ -217,7 +216,7 @@ class NodeHandler(bmchandler.NodeHandler):
wc.set_header('ST2', st2)
return wc
def config(self, nodename):
async def config(self, nodename):
# SMM for now has to reset to assure configuration applies
cd = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
@@ -255,7 +254,7 @@ class NodeHandler(bmchandler.NodeHandler):
# Switch to full web based configuration, to mitigate risks with the SMM
wc = self._webconfigcreds(username, passwd)
self._webconfigrules(wc)
self._webconfignet(wc, nodename)
await self._webconfignet(wc, nodename)
# notes for smm:
@@ -12,16 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.discovery.handlers.redfishbmc as redfishbmc
import eventlet.support.greendns
import confluent.util as util
webclient = eventlet.import_patched('pyghmi.util.webclient')
getaddrinfo = eventlet.support.greendns.getaddrinfo
class NodeHandler(redfishbmc.NodeHandler):
devname = 'SMM3'
@@ -45,19 +40,19 @@ class NodeHandler(redfishbmc.NodeHandler):
return ('USERID', 'PASSW0RD')
def remote_nodecfg(nodename, cfm):
async def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
nodename, 'hardwaremanagement.manager')
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
'value', None)
ipaddr = ipaddr.split('/', 1)[0]
ipaddr = getaddrinfo(ipaddr, 0)[0][-1]
ipaddr = await asyncio.get_event_loop().getaddrinfo(ipaddr, 0)[0][-1]
if not ipaddr:
raise Exception('Cannot remote configure a system without known '
'address')
info = {'addresses': [ipaddr]}
nh = NodeHandler(info, cfm)
nh.config(nodename)
await nh.config(nodename)
if __name__ == '__main__':
@@ -67,5 +62,5 @@ if __name__ == '__main__':
info = {'addresses': [[sys.argv[1]]]}
print(repr(info))
testr = NodeHandler(info, c)
testr.config(sys.argv[2])
asyncio.run(testr.config(sys.argv[2]))
@@ -12,21 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.discovery.handlers.generic as generic
import confluent.exceptions as exc
import confluent.netutil as netutil
import confluent.util as util
import eventlet
import eventlet.support.greendns
import json
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
import socket
from urllib.parse import urlencode
getaddrinfo = eventlet.support.greendns.getaddrinfo
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
class NodeHandler(generic.NodeHandler):
devname = 'TSM'
@@ -45,9 +40,11 @@ class NodeHandler(generic.NodeHandler):
self.atdefault = True
super(NodeHandler, self).__init__(info, configmanager)
def scan(self):
c = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
i = c.grab_json_response('/redfish/v1/')
async def scan(self):
await self.get_https_cert()
c = webclient.WebConnection(
self.ipaddr, 443, verifycallback=self.validate_cert)
i = await c.grab_json_response('/redfish/v1/')
uuid = i.get('UUID', None)
if uuid:
self.info['uuid'] = uuid.lower()
@@ -58,20 +55,21 @@ class NodeHandler(generic.NodeHandler):
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def _get_wc(self):
async def _get_wc(self):
authdata = { # start by trying factory defaults
'username': self.DEFAULT_USER,
'password': self.DEFAULT_PASS,
}
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
await self.get_https_cert()
wc = webclient.WebConnection(self.ipaddr, 443, verifycallback=self.validate_cert)
wc.set_header('Content-Type', 'application/json')
authmode = 0
if not self.trieddefault:
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
rsp, status = await wc.grab_json_response_with_status('/api/session', authdata)
if status == 403:
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
authmode = 1
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
rsp, status = await wc.grab_json_response_with_status('/api/session', urlencode(authdata))
else:
authmode = 2
if status > 400:
@@ -89,31 +87,31 @@ class NodeHandler(generic.NodeHandler):
rpasschange = {
'Password': self.targpass,
}
rwc = webclient.SecureHTTPConnection(
rwc = webclient.WebConnection(
self.ipaddr, 443,
verifycallback=self.validate_cert)
rwc.set_basic_credentials(authdata['username'],
authdata['password'])
rwc.set_header('If-Match', '*')
rwc.set_header('Content-Type', 'application/json')
rsp, status = rwc.grab_json_response_with_status(
rsp, status = await rwc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts/1',
rpasschange, method='PATCH')
if status >= 200 and status < 300:
authdata['password'] = self.targpass
eventlet.sleep(10)
await asyncio.sleep(10)
else:
if b'[web.lua] Error in RequestHandler, thread' in rsp:
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', passchange)
rsp, status = await wc.grab_json_response_with_status('/api/reset-pass', passchange)
else:
raise Exception("Redfish may not have been ready yet" + repr(rsp))
else:
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', urlencode(passchange))
rsp, status = await wc.grab_json_response_with_status('/api/reset-pass', urlencode(passchange))
authdata['password'] = self.targpass
if authmode == 2:
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
rsp, status = await wc.grab_json_response_with_status('/api/session', authdata)
else:
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
rsp, status = await wc.grab_json_response_with_status('/api/session', urlencode(authdata))
self.csrftok = rsp['CSRFToken']
self.channel = rsp['channel']
self.curruser = self.DEFAULT_USER
@@ -129,10 +127,10 @@ class NodeHandler(generic.NodeHandler):
authdata['username'] = self.curruser
authdata['password'] = self.currpass
if authmode != 1:
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
rsp, status = await wc.grab_json_response_with_status('/api/session', authdata)
if authmode == 1 or status == 403:
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
rsp, status = await wc.grab_json_response_with_status('/api/session', urlencode(authdata))
if status != 200:
return None
self.csrftok = rsp['CSRFToken']
@@ -141,10 +139,10 @@ class NodeHandler(generic.NodeHandler):
authdata['username'] = self.targuser
authdata['password'] = self.targpass
if authmode != 1:
rsp, status = wc.grab_json_response_with_status('/api/session', authdata)
rsp, status = await wc.grab_json_response_with_status('/api/session', authdata)
if authmode == 1 or status == 403:
wc.set_header('Content-Type', 'application/x-www-form-urlencoded')
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
rsp, status = await wc.grab_json_response_with_status('/api/session', urlencode(authdata))
if status != 200:
return None
self.curruser = self.targuser
@@ -153,7 +151,7 @@ class NodeHandler(generic.NodeHandler):
self.channel = rsp['channel']
return wc
def config(self, nodename):
async def config(self, nodename):
self.nodename = nodename
creds = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
@@ -167,7 +165,7 @@ class NodeHandler(generic.NodeHandler):
passwd = util.stringify(passwd)
self.targuser = user
self.targpass = passwd
wc = self._get_wc()
wc = await self._get_wc()
wc.set_header('X-CSRFTOKEN', self.csrftok)
curruserinfo = {}
authupdate = False
@@ -202,7 +200,7 @@ class NodeHandler(generic.NodeHandler):
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newip = newip.split('/', 1)[0]
newipinfo = getaddrinfo(newip, 0)[0]
newipinfo = socket.getaddrinfo(newip, 0)[0]
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
@@ -214,7 +212,7 @@ class NodeHandler(generic.NodeHandler):
if net['channel_number'] == self.channel:
# we have found the interface to potentially manipulate
if net['ipv4_address'] != newip:
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
netconfig = await netutil.get_nic_config(self.configmanager, nodename, ip=newip)
newmask = netutil.cidr_to_mask(netconfig['prefix'])
net['ipv4_address'] = newip
net['ipv4_subnet'] = newmask
@@ -239,7 +237,7 @@ def remote_nodecfg(nodename, cfm):
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
'value', None)
ipaddr = ipaddr.split('/', 1)[0]
ipaddr = getaddrinfo(ipaddr, 0)[0][-1]
ipaddr = socket.getaddrinfo(ipaddr, 0)[0][-1]
if not ipaddr:
raise Exception('Cannot remote configure a system without known '
'address')
@@ -254,4 +252,4 @@ if __name__ == '__main__':
info = {'addresses': [[sys.argv[1]]] }
print(repr(info))
testr = NodeHandler(info, c)
testr.config(sys.argv[2])
testr.config(sys.argv[2])
@@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import base64
import codecs
import confluent.discovery.handlers.imm as immhandler
@@ -20,15 +21,12 @@ import confluent.exceptions as exc
import confluent.netutil as netutil
import confluent.util as util
import errno
import eventlet
import eventlet.support.greendns
import json
import os
import pyghmi.exceptions as pygexc
import eventlet.green.socket as socket
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.exceptions as pygexc
import socket
import aiohmi.util.webclient as webclient
import struct
getaddrinfo = eventlet.support.greendns.getaddrinfo
def fixuuid(baduuid):
@@ -40,7 +38,8 @@ def fixuuid(baduuid):
uuid = (a[:8], a[8:12], a[12:16], baduuid[19:23], baduuid[24:])
return '-'.join(uuid).lower()
class LockedUserException(Exception):
class LockedUserException(BaseException):
pass
@@ -58,19 +57,19 @@ class NodeHandler(immhandler.NodeHandler):
self._currcreds = (None, None)
super(NodeHandler, self).__init__(info, configmanager)
@property
def ipaddr(self):
async def get_ipaddr(self):
if not self._ipaddr:
cloop = asyncio.get_running_loop()
lla = self.info.get('linklocal', '')
tmplla = None
if lla:
for idx in util.list_interface_indexes():
tmplla = '{0}%{1}'.format(lla, idx)
addr = socket.getaddrinfo(tmplla, 443, 0, socket.SOCK_STREAM)[0][4]
addr = (await cloop.getaddrinfo(tmplla, 443, 0, socket.SOCK_STREAM))[0][4]
try:
tsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
tsock.settimeout(1)
tsock.connect(addr)
tsock.setblocking(0)
await asyncio.wait_for(cloop.sock_connect(tsock, addr), timeout=1)
tsock.close()
break
except Exception:
@@ -89,12 +88,13 @@ class NodeHandler(immhandler.NodeHandler):
def probe(self):
return None
def scan(self):
ip, port = self.get_web_port_and_ip()
c = webclient.SecureHTTPConnection(ip, port,
async def scan(self):
ip, port = await self.get_web_port_and_ip()
await self.get_https_cert()
c = webclient.WebConnection(ip, port,
verifycallback=self.validate_cert)
try:
i = c.grab_json_response('/api/providers/logoninfo')
i = await c.grab_json_response('/api/providers/logoninfo')
except Exception:
return
modelname = i.get('items', [{}])[0].get('machine_name', None)
@@ -135,7 +135,7 @@ class NodeHandler(immhandler.NodeHandler):
if slot != 0:
self.info['enclosure.bay'] = slot
def preconfig(self, possiblenode):
async def preconfig(self, possiblenode):
self.tmpnodename = possiblenode
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
@@ -156,17 +156,17 @@ class NodeHandler(immhandler.NodeHandler):
disableipmi = False
if currfirm >= 3:
# IPMI is disabled and we need it, also we need to go to *some* password
wc = self.wc
wc = await self.get_wc()
if not wc:
# We cannot try to enable SMM here without risking real credentials
# on the wire to untrusted parties
return
wc.grab_json_response('/api/providers/logout')
await wc.grab_json_response('/api/providers/logout')
wc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
rsp = wc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
rsp = await wc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
disableipmi = True
_, _ = wc.grab_json_response_with_status(
_, _ = await wc.grab_json_response_with_status(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
ipmicmd = None
@@ -179,13 +179,13 @@ class NodeHandler(immhandler.NodeHandler):
str(e) != 'Session no longer connected'):
# raise an issue if anything other than to be expected
if disableipmi:
_, _ = wc.grab_json_response_with_status(
_, _ = await wc.grab_json_response_with_status(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
raise
self.trieddefault = True
if disableipmi:
_, _ = wc.grab_json_response_with_status(
_, _ = await wc.grab_json_response_with_status(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
#TODO: decide how to clean out if important
@@ -199,122 +199,105 @@ class NodeHandler(immhandler.NodeHandler):
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def get_webclient(self, username, password, newpassword):
async def get_webclient(self, username, password, newpassword):
wc = self._wc.dupe()
try:
wc.connect()
except socket.error as se:
if se.errno != errno.ECONNREFUSED:
raise
return (None, None)
pwdchanged = False
adata = json.dumps({'username': util.stringify(username),
'password': util.stringify(password)
})
adata = {'username': util.stringify(username),
'password': util.stringify(password)}
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
rsp, status = wc.grab_json_response_with_status('/api/providers/get_nonce', {})
rsp, status = await wc.grab_json_response_with_status('/api/providers/get_nonce', {})
nonce = None
if status == 200:
nonce = rsp.get('nonce', None)
headers['Content-Security-Policy'] = 'nonce={0}'.format(nonce)
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
try:
rspdata = json.loads(rsp.read())
except Exception:
rspdata = {}
if rsp.status != 200 and password == 'PASSW0RD':
rspdata, status = await wc.grab_json_response_with_status('/api/login', adata, headers=headers)
if status != 200 and password == 'PASSW0RD':
rspdata = json.loads(rspdata)
if rspdata.get('locktime', 0) > 0:
raise LockedUserException(
'The user "{0}" has been locked out for too many incorrect password attempts'.format(username))
adata = json.dumps({
adata = {
'username': username,
'password': newpassword,
})
}
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
if nonce:
wc.request('POST', '/api/providers/get_nonce', '{}')
rsp = wc.getresponse()
tokbody = rsp.read()
if rsp.status == 200:
rsp = json.loads(tokbody)
nonce = rsp.get('nonce', None)
rsp = await wc.grab_json_response('/api/providers/get_nonce', {})
nonce = rsp.get('nonce', None)
if nonce:
headers['Content-Security-Policy'] = 'nonce={0}'.format(nonce)
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
try:
rspdata = json.loads(rsp.read())
except Exception:
rspdata = {}
if rsp.status == 200:
rspdata = await wc.grab_json_response('/api/login', adata, headers=headers)
if rspdata:
pwdchanged = True
password = newpassword
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
if '_csrf_token' in wc.cookies:
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
wc.grab_json_response_with_status('/api/providers/logout')
for cookie in wc.cookies:
if cookie.key.lower() == '_csrf_token':
wc.set_header('X-XSRF-TOKEN', cookie.value)
await wc.grab_json_response_with_status('/api/providers/logout')
else:
if rspdata.get('locktime', 0) > 0:
raise LockedUserException(
'The user "{0}" has been locked out for too many incorrect password attempts'.format(username))
return (None, rspdata)
if rsp.status == 200:
if status == 200:
self._currcreds = (username, password)
wc.set_basic_credentials(username, password)
wc.set_header('Content-Type', 'application/json')
wc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
if '_csrf_token' in wc.cookies:
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
for cookie in wc.cookies:
if cookie.key.lower() == '_csrf_token':
wc.set_header('X-XSRF-TOKEN', cookie.value)
if rspdata.get('pwchg_required', None) == 'true':
if newpassword is None:
# a normal login hit expired condition
tmppassword = 'Tmp42' + password[5:]
wc.request('POST', '/api/function', json.dumps(
{'USER_UserPassChange': '1,{0}'.format(tmppassword)}))
rsp = wc.getresponse()
rsp.read()
await wc.grab_json_response(
'/api/function',
{'USER_UserPassChange': '1,{0}'.format(tmppassword)})
# We must step down change interval and reusecycle to restore password
wc.grab_json_response('/api/dataset', {'USER_GlobalMinPassChgInt': '0', 'USER_GlobalMinPassReuseCycle': '0'})
wc.request('POST', '/api/function', json.dumps(
{'USER_UserPassChange': '1,{0}'.format(password)}))
rsp = wc.getresponse()
rsp.read()
await wc.grab_json_response(
'/api/dataset',
{'USER_GlobalMinPassChgInt': '0', 'USER_GlobalMinPassReuseCycle': '0'})
await wc.grab_json_response(
'/api/function',
{'USER_UserPassChange': '1,{0}'.format(password)})
return (wc, {})
wc.request('POST', '/api/function', json.dumps(
{'USER_UserPassChange': '1,{0}'.format(newpassword)}))
rsp = wc.getresponse()
rsp.read()
if rsp.status != 200:
rsp, status = await wc.grab_json_response_with_status(
'/api/function',
{'USER_UserPassChange': '1,{0}'.format(newpassword)})
if status != 200:
return (None, None)
wc.grab_json_response_with_status('/api/providers/logout')
await wc.grab_json_response_with_status('/api/providers/logout')
self._currcreds = (username, newpassword)
wc.set_basic_credentials(username, newpassword)
pwdchanged = True
if '_csrf_token' in wc.cookies:
wc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
for cookie in wc.cookies:
if cookie.key.lower() == '_csrf_token':
wc.set_header('X-XSRF-TOKEN', cookie.value)
if pwdchanged:
# Remove the minimum change interval, to allow sane
# password changes after provisional changes
wc = self.wc
self.set_password_policy('', wc)
wc = await self.get_wc()
await self.set_password_policy('', wc)
return (wc, pwdchanged)
elif rspdata.get('locktime', 0) > 0:
raise LockedUserException(
'The user "{0}" has been locked out by too many incorrect password attempts'.format(username))
return (None, rspdata)
@property
def wc(self):
async def get_wc(self):
passwd = None
isdefault = True
errinfo = {}
if self._wc is None:
ip, port = self.get_web_port_and_ip()
self._wc = webclient.SecureHTTPConnection(
ip, port = await self.get_web_port_and_ip()
await self.get_https_cert()
self._wc = webclient.WebConnection(
ip, port, verifycallback=self.validate_cert)
self._wc.connect()
# self._wc.connect()
nodename = None
if self.nodename:
nodename = self.nodename
@@ -323,7 +306,7 @@ class NodeHandler(immhandler.NodeHandler):
nodename = None
inpreconfig = True
if self._currcreds[0] is not None:
wc, pwdchanged = self.get_webclient(self._currcreds[0], self._currcreds[1], None)
wc, pwdchanged = await self.get_webclient(self._currcreds[0], self._currcreds[1], None)
if wc:
return wc
if nodename:
@@ -346,7 +329,7 @@ class NodeHandler(immhandler.NodeHandler):
# (TempW0rd42)
passwd = 'TempW0rd42'
try:
wc, pwdchanged = self.get_webclient('USERID', 'PASSW0RD', passwd)
wc, pwdchanged = await self.get_webclient('USERID', 'PASSW0RD', passwd)
except LockedUserException as lue:
wc = None
pwdchanged = 'The user "USERID" has been locked out by too many incorrect password attempts'
@@ -367,11 +350,11 @@ class NodeHandler(immhandler.NodeHandler):
if self.tmppasswd:
if savedexc:
raise savedexc
wc, errinfo = self.get_webclient('USERID', self.tmppasswd, passwd)
wc, errinfo = await self.get_webclient('USERID', self.tmppasswd, passwd)
else:
if user == 'USERID' and savedexc:
raise savedexc
wc, errinfo = self.get_webclient(user, passwd, None)
wc, errinfo = await self.get_webclient(user, passwd, None)
if wc:
return wc
else:
@@ -379,7 +362,7 @@ class NodeHandler(immhandler.NodeHandler):
raise Exception('The stored confluent password for user "{}" was not accepted by the XCC'.format(user))
raise Exception('Error connecting to webservice: ' + repr(errinfo))
def set_password_policy(self, strruleset, wc):
async def set_password_policy(self, strruleset, wc):
ruleset = {'USER_GlobalMinPassChgInt': '0'}
for rule in strruleset.split(','):
if '=' not in rule:
@@ -400,20 +383,20 @@ class NodeHandler(immhandler.NodeHandler):
if name.lower() == 'reuse':
ruleset['USER_GlobalMinPassReuseCycle'] = value
try:
wc.grab_json_response('/api/dataset', ruleset)
await wc.grab_json_response('/api/dataset', ruleset)
except Exception as e:
print(repr(e))
pass
def _get_next_userid(self, wc):
userinfo = wc.grab_json_response('/api/dataset/imm_users')
async def _get_next_userid(self, wc):
userinfo = await wc.grab_json_response('/api/dataset/imm_users')
userinfo = userinfo['items'][0]['users']
for user in userinfo:
if user['users_user_name'] == '':
return user['users_user_id']
def create_tmp_account(self, wc):
rsp, status = wc.grab_json_response_with_status('/redfish/v1/AccountService/Accounts')
async def create_tmp_account(self, wc):
rsp, status = await wc.grab_json_response_with_status('/redfish/v1/AccountService/Accounts')
if status != 200:
raise Exception("Unable to list current accounts")
usednames = set([])
@@ -423,25 +406,25 @@ class NodeHandler(immhandler.NodeHandler):
for acct in rsp.get("Members", []):
url = acct.get("@odata.id", None)
if url:
uinfo = wc.grab_json_response(url)
uinfo = await wc.grab_json_response(url)
usednames.add(uinfo.get('UserName', None))
if tmpnam in usednames:
raise Exception("Tmp account already exists")
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts',
{'UserName': tmpnam, 'Password': tpass, 'RoleId': 'Administrator'})
if status >= 300:
raise Exception("Failure creating tmp account: " + repr(rsp))
tmpurl = rsp['@odata.id']
wc.set_basic_credentials(tmpnam, tpass)
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
tmpurl, {'Password': ntpass}, method='PATCH')
wc.set_basic_credentials(tmpnam, ntpass)
return tmpurl
def _setup_xcc_account(self, username, passwd, wc):
userinfo = wc.grab_json_response('/api/dataset/imm_users')
async def _setup_xcc_account(self, username, passwd, wc):
userinfo = await wc.grab_json_response('/api/dataset/imm_users')
uid = None
for user in userinfo['items'][0]['users']:
if user['users_user_name'] == username:
@@ -456,64 +439,63 @@ class NodeHandler(immhandler.NodeHandler):
raise Exception("XCC has neither the default user nor configured user")
# The following will work if the password is force change or normal..
if self._needpasswordchange and self.tmppasswd != passwd:
wc.grab_json_response('/api/function',
await wc.grab_json_response('/api/function',
{'USER_UserPassChange': '{0},{1}'.format(uid, passwd)})
if username != 'USERID':
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/api/function',
{'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,'.format(uid, username)})
if status == 200 and rsp.get('return', 0) == 762:
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/api/function',
{'USER_UserModify': '{0},{1},,1,Administrator,0,0,0,0,,8,'.format(uid, username)})
elif status == 200 and rsp.get('return', 0) == 13:
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/api/function',
{'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,,,'.format(uid, username)})
if status == 200 and rsp.get('return', 0) == 13:
wc.grab_json_response('/api/providers/logout')
await wc.grab_json_response('/api/providers/logout')
wc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
status = 503
tries = 2
tmpaccount = None
while status != 200:
tries -= 1
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts/{0}'.format(uid))
if status >= 500:
if tries < 0:
raise Exception('Redfish account management failure')
eventlet.sleep(30)
await asyncio.sleep(30)
continue
rsp, status = wc.grab_json_response_with_status(
rsp, status = await wc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts/{0}'.format(uid),
{'UserName': username}, method='PATCH')
if status != 200:
rsp = json.loads(rsp)
if rsp.get('error', {}).get('code', 'Unknown') in ('Base.1.8.GeneralError', 'Base.1.12.GeneralError', 'Base.1.14.GeneralError', 'Base.1.18.GeneralError', 'Base.1.21.GeneralError'):
if tries:
eventlet.sleep(4)
await asyncio.sleep(4)
elif tmpaccount:
wc.grab_json_response_with_status(tmpaccount, method='DELETE')
await wc.grab_json_response_with_status(tmpaccount, method='DELETE')
raise Exception('Failed renaming main account')
else:
tmpaccount = self.create_tmp_account(wc)
tmpaccount = await self.create_tmp_account(wc)
tries = 8
else:
break
if tmpaccount:
wc.set_basic_credentials(username, passwd)
wc.grab_json_response_with_status(tmpaccount, method='DELETE')
await wc.grab_json_response_with_status(tmpaccount, method='DELETE')
self.tmppasswd = None
self._currcreds = (username, passwd)
return
self.tmppasswd = None
wc.grab_json_response('/api/providers/logout')
await wc.grab_json_response('/api/providers/logout')
self._currcreds = (username, passwd)
def _convert_sha256account(self, user, passwd, wc):
async def _convert_sha256account(self, user, passwd, wc):
# First check if the specified user is sha256...
userinfo = wc.grab_json_response('/api/dataset/imm_users')
userinfo = await wc.grab_json_response('/api/dataset/imm_users')
curruser = None
uid = None
user = util.stringify(user)
@@ -524,62 +506,59 @@ class NodeHandler(immhandler.NodeHandler):
break
if curruser and curruser.get('users_pass_is_sha256', 0):
self._wc = None
wc = self.wc
wc = await self.get_wc()
nwc = wc.dupe()
# Have to convert it for being useful with most Lenovo automation tools
# This requires deleting the account entirely and trying again
tmpuid = self._get_next_userid(wc)
tmpuid = await self._get_next_userid(wc)
try:
tpass = base64.b64encode(os.urandom(9)) + 'Iw47$'
userparams = "{0},6pmu0ezczzcp,{1},1,4,0,0,0,0,,8,".format(tmpuid, tpass)
result = wc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
wc.grab_json_response('/api/providers/logout')
adata = json.dumps({
result = await wc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
await wc.grab_json_response('/api/providers/logout')
adata = {
'username': '6pmu0ezczzcp',
'password': tpass,
})
}
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/json'}
wc.request('POST', '/api/providers/get_nonce', '{}')
rsp = wc.getresponse()
tokbody = rsp.read()
if rsp.status == 200:
rsp = json.loads(tokbody)
rsp, status = await wc.grab_json_response('/api_providers/get_nonce', {})
if status == 200:
nonce = rsp.get('nonce', None)
headers['Content-Security-Policy'] = 'nonce={0}'.format(nonce)
nwc.request('POST', '/api/login', adata, headers)
rsp = nwc.getresponse()
if rsp.status == 200:
rspdata = json.loads(rsp.read())
rsp, status = await nwc.grab_json_response_with_status('/api/login', adata, headers=headers)
if status == 200:
rspdata = rsp
nwc.set_header('Content-Type', 'application/json')
nwc.set_header('Authorization', 'Bearer ' + rspdata['access_token'])
if '_csrf_token' in wc.cookies:
nwc.set_header('X-XSRF-TOKEN', wc.cookies['_csrf_token'])
for cookie in wc.cookies:
if cookie.key.lower() == '_csrf_token':
nwc.set_header('X-XSRF-TOKEN', cookie.value)
if rspdata.get('reason', False):
newpass = base64.b64encode(os.urandom(9)) + 'q4J$'
nwc.grab_json_response(
await nwc.grab_json_response(
'/api/function',
{'USER_UserPassChange': '{0},{1}'.format(tmpuid, newpass)})
nwc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(curruser['users_user_id'], user)})
await nwc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(curruser['users_user_id'], user)})
userparams = "{0},{1},{2},1,4,0,0,0,0,,8,".format(curruser['users_user_id'], user, tpass)
nwc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
nwc.grab_json_response('/api/providers/logout')
nwc, pwdchanged = self.get_webclient(user, tpass, passwd)
await nwc.grab_json_response('/api/function', {'USER_UserCreate': userparams})
await nwc.grab_json_response('/api/providers/logout')
nwc, pwdchanged = await self.get_webclient(user, tpass, passwd)
if not nwc:
if not pwdchanged:
pwdchanged = 'Unknown'
raise Exception('Error converting from sha356account: ' + repr(pwdchanged))
if not pwdchanged:
nwc.grab_json_response(
await nwc.grab_json_response(
'/api/function',
{'USER_UserPassChange': '{0},{1}'.format(curruser['users_user_id'], passwd)})
nwc.grab_json_response('/api/providers/logout')
await nwc.grab_json_response('/api/providers/logout')
finally:
self._wc = None
wc = self.wc
wc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(tmpuid, '6pmu0ezczzcp')})
wc.grab_json_response('/api/providers/logout')
wc = await self.get_wc()
await wc.grab_json_response('/api/function', {'USER_UserDelete': "{0},{1}".format(tmpuid, '6pmu0ezczzcp')})
await wc.grab_json_response('/api/providers/logout')
def config(self, nodename, reset=False):
async def config(self, nodename, reset=False):
self.nodename = nodename
cd = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
@@ -588,7 +567,8 @@ class NodeHandler(immhandler.NodeHandler):
True)
cd = cd.get(nodename, {})
targbmc = cd.get('hardwaremanagement.manager', {}).get('value', '')
if not self.ipaddr.startswith('fe80::') and (targbmc.startswith('fe80::') or not targbmc):
myipaddr = await self.get_ipaddr()
if not myipaddr.startswith('fe80::') and (targbmc.startswith('fe80::') or not targbmc):
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
@@ -598,32 +578,32 @@ class NodeHandler(immhandler.NodeHandler):
nodename, 'discovery.passwordrules')
strruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
wc = self.wc
wc = await self.get_wc()
creds = self.configmanager.get_node_attributes(
self.nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
user, passwd, isdefault = self.get_node_credentials(nodename, creds, 'USERID', 'PASSW0RD')
self.set_password_policy(strruleset, wc)
await self.set_password_policy(strruleset, wc)
if self._atdefaultcreds:
if isdefault and self.tmppasswd:
raise Exception(
'Request to use default credentials, but refused by target after it has been changed to {0}'.format(self.tmppasswd))
if not isdefault:
self._setup_xcc_account(user, passwd, wc)
wc = self.wc
self._convert_sha256account(user, passwd, wc)
await self._setup_xcc_account(user, passwd, wc)
wc = await self.get_wc()
await self._convert_sha256account(user, passwd, wc)
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
nwc = wc.dupe()
nwc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
rsp = nwc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
rsp = await nwc.grab_json_response('/redfish/v1/Managers/1/NetworkProtocol')
if not rsp.get('IPMI', {}).get('ProtocolEnabled', True):
# User has indicated IPMI support, but XCC is currently disabled
# change XCC to be consistent
_, _ = nwc.grab_json_response_with_status(
_, _ = await nwc.grab_json_response_with_status(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
rsp, status = nwc.grab_json_response_with_status(
rsp, status = await nwc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts/1')
if status == 200:
allowable = rsp.get('AccountTypes@Redfish.AllowableValues', [])
@@ -634,35 +614,36 @@ class NodeHandler(immhandler.NodeHandler):
'AccountTypes': current,
'Password': self._currcreds[1]
}
rsp, status = nwc.grab_json_response_with_status(
rsp, status = await nwc.grab_json_response_with_status(
'/redfish/v1/AccountService/Accounts/1',
updateinf, method='PATCH')
if targbmc and not targbmc.startswith('fe80::'):
attribsuffix = ''
newip = targbmc.split('/', 1)[0]
newipinfo = getaddrinfo(newip, 0)[0]
newip = newipinfo[-1][0]
cloop = asyncio.get_running_loop()
newipinfo = await cloop.getaddrinfo(newip, 0)
newip = newipinfo[0][-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=targbmc)
netconfig = await netutil.get_nic_config(self.configmanager, nodename, ip=targbmc)
newmask = netutil.cidr_to_mask(netconfig['prefix'])
currinfo = wc.grab_json_response('/api/providers/logoninfo')
currinfo = await wc.grab_json_response('/api/providers/logoninfo')
currip = currinfo.get('items', [{}])[0].get('ipv4_address', '')
curreth1 = wc.grab_json_response('/api/dataset/imm_ethernet')
curreth1 = await wc.grab_json_response('/api/dataset/imm_ethernet')
if curreth1:
if self.ipaddr.startswith('fe80::'):
if myipaddr.startswith('fe80::'):
ipkey = 'ipv6_link_local_address'
elif '.' in self.ipaddr:
elif '.' in myipaddr:
ipkey = 'ipv4_address'
else:
raise Exception('Non-Link-Local IPv6 TODO')
nic1ip = curreth1.get('items', [{}])[0].get(ipkey, None)
if nic1ip != self.ipaddr:
if nic1ip != myipaddr:
# check second nic instead
curreth2 = wc.grab_json_response('/api/dataset/imm_ethernet_2')
curreth2 = await wc.grab_json_response('/api/dataset/imm_ethernet_2')
if curreth2 and curreth2.get('items', [{}])[0].get('if_second_port_exist', 0):
nic2ip = curreth2.get('items', [{}])[0].get(ipkey + '_2', None)
if nic2ip != self.ipaddr:
if nic2ip != myipaddr:
raise Exception("Unable to determine which NIC is active")
# ok, second nic is active, target it
currip = curreth2.get('items', [{}])[0].get("ipv4_address", None)
@@ -675,21 +656,21 @@ class NodeHandler(immhandler.NodeHandler):
}
if netconfig['ipv4_gateway']:
statargs['ENET_IPv4GatewayIPAddr'] = netconfig['ipv4_gateway']
elif not netutil.address_is_local(newip):
elif not await netutil.address_is_local(newip):
raise exc.InvalidArgumentException('Will not remotely configure a device with no gateway')
if attribsuffix:
for currkey in list(statargs):
statargs[currkey + attribsuffix] = statargs[currkey]
del statargs[currkey]
netset, status = wc.grab_json_response_with_status('/api/dataset', statargs)
netset, status = await wc.grab_json_response_with_status('/api/dataset', statargs)
elif self.ipaddr.startswith('fe80::'):
self.configmanager.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
elif myipaddr.startswith('fe80::'):
await self.configmanager.set_node_attributes(
{nodename: {'hardwaremanagement.manager': myipaddr}})
else:
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address (No IPv6 Link Local detected)')
wc.grab_json_response('/api/providers/logout')
await wc.grab_json_response('/api/providers/logout')
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
@@ -702,27 +683,38 @@ class NodeHandler(immhandler.NodeHandler):
'value', None)
# ok, set the uuid of the manager...
if em:
self.configmanager.set_node_attributes(
await self.configmanager.set_node_attributes(
{em: {'id.uuid': enclosureuuid}})
async 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
await util.check_call('/opt/confluent/bin/nodecertutil', nodename, 'signbmccert', '--days', '47')
def remote_nodecfg(nodename, cfm):
async def remote_nodecfg(nodename, cfm):
cfg = cfm.get_node_attributes(
nodename, 'hardwaremanagement.manager')
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
'value', None)
ipaddr = ipaddr.split('/', 1)[0]
ipaddr = getaddrinfo(ipaddr, 0)[0][-1]
cloop = asyncio.get_running_loop()
newipinfo = await cloop.getaddrinfo(ipaddr, 0)
ipaddr = newipinfo[0][-1][0]
if not ipaddr:
raise Exception('Cannot remote configure a system without known '
'address')
info = {'addresses': [ipaddr]}
ipaddr = ipaddr[0]
wc = webclient.SecureHTTPConnection(
wc = webclient.WebConnection(
ipaddr, 443, verifycallback=lambda x: True)
rsp = wc.grab_json_response('/DeviceDescription.json')
rsp = await wc.grab_json_response('/DeviceDescription.json')
if isinstance(rsp, list):
nh = NodeHandler(info, cfm)
else:
nh = xcc3handler.NodeHandler(info, cfm)
nh.config(nodename)
await nh.config(nodename)
@@ -13,14 +13,9 @@
# limitations under the License.
import confluent.discovery.handlers.redfishbmc as redfishbmc
import eventlet.support.greendns
import confluent.util as util
webclient = eventlet.import_patched('pyghmi.util.webclient')
getaddrinfo = eventlet.support.greendns.getaddrinfo
import socket
import aiohmi.util.webclient as webclient
class NodeHandler(redfishbmc.NodeHandler):
@@ -32,12 +27,13 @@ class NodeHandler(redfishbmc.NodeHandler):
def get_manager_url(self, wc):
return '/redfish/v1/Managers/1'
def scan(self):
ip, port = self.get_web_port_and_ip()
c = webclient.SecureHTTPConnection(ip, port,
async def scan(self):
ip, port = await self.get_web_port_and_ip()
await self.get_https_cert()
c = webclient.WebConnection(ip, port,
verifycallback=self.validate_cert)
c.set_header('Accept', 'application/json')
i = c.grab_json_response('/api/providers/logoninfo')
i = await c.grab_json_response('/api/providers/logoninfo')
modelname = i.get('items', [{}])[0].get('machine_name', None)
if modelname:
self.info['modelname'] = modelname
@@ -87,7 +83,7 @@ def remote_nodecfg(nodename, cfm):
ipaddr = cfg.get(nodename, {}).get('hardwaremanagement.manager', {}).get(
'value', None)
ipaddr = ipaddr.split('/', 1)[0]
ipaddr = getaddrinfo(ipaddr, 0)[0][-1]
ipaddr = socket.getaddrinfo(ipaddr, 0)[0][-1]
if not ipaddr:
raise Exception('Cannot remote configure a system without known '
'address')
@@ -28,6 +28,7 @@
# NTS: ssdp:alive
import asyncio
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
import confluent.neighutil as neighutil
@@ -35,16 +36,15 @@ import confluent.noderange as noderange
import confluent.util as util
import confluent.log as log
import confluent.netutil as netutil
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
import eventlet.greenpool as gp
import confluent.tasks as tasks
import socket
import os
import time
import struct
import traceback
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
mcastv4addr = '224.0.0.251'
mcastv6addr = 'ff02::fb'
@@ -94,14 +94,14 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha
}
if sdata.get('ttl', 0) == 0:
if byehandler:
eventlet.spawn_n(check_fish_handler, byehandler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer)
tasks.spawn(check_fish_handler(byehandler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer))
return 1
if handler:
eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer)
tasks.spawn(check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer))
return 2
def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer):
retdata = check_fish(('/redfish/v1/', peerdata))
async def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer):
retdata = await check_fish(('/redfish/v1/', peerdata))
if retdata:
known_peers.add(peer)
newmacs.add(mac)
@@ -109,7 +109,7 @@ def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress
machandlers[mac] = handler
def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
async def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
"""Watch for unsolicited mDNS answers
The handler shall be called on any service coming online.
@@ -131,7 +131,7 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
net6.bind(('', 5353))
net4.bind(('', 5353))
try:
active_scan(handler, protocol)
await active_scan(handler, protocol)
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
@@ -151,43 +151,58 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
# errno 98 can happen if aliased, skip for now
raise
peerbymacaddress = {}
newmacs = set([])
deferrednotifies = []
machandlers = {}
pktq = asyncio.Queue()
cloop = asyncio.get_running_loop()
cloop.add_reader(net4, _relay_pkt, net4, pktq)
cloop.add_reader(net6, _relay_pkt, net6, pktq)
while True:
try:
newmacs = set([])
deferrednotifies = []
machandlers = {}
r = select.select((net4, net6), (), (), 60)
if r:
r = r[0]
recent_peers = set([])
while r and len(deferrednotifies) < 256:
for s in r:
(rsp, peer) = s.recvfrom(9000)
if peer in recent_peers:
newmacs.clear()
deferrednotifies.clear()
machandlers.clear()
timeout = None
srp = await pktq.get()
recent_peers.clear()
while srp and len(deferrednotifies) < 256:
srp = None
if timeout is None:
srp = await pktq.get()
else:
try:
srp = await asyncio.wait_for(pktq.get(), timeout=timeout)
except asyncio.exceptions.TimeoutError:
break
timeout = 0.2
s, rsp, peer = srp
if peer in recent_peers:
continue
mac = await neighutil.get_hwaddr(peer[0])
if mac == False:
continue
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.setblocking(1)
s.sendto(b'\x00', probepeer)
except Exception:
continue
mac = neighutil.get_hwaddr(peer[0])
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.sendto(b'\x00', probepeer)
except Exception:
continue
deferrednotifies.append((peer, rsp))
datum = _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
if datum == 2:
recent_peers.add(peer)
r = select.select((net4, net6), (), (), 1.5)
if r:
r = r[0]
deferrednotifies.append((peer, rsp))
continue
datum = _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
if datum == 2:
recent_peers.add(peer)
if deferrednotifies:
eventlet.sleep(2.2)
await asyncio.sleep(2.2)
for peerrsp in deferrednotifies:
peer, rsp = peerrsp
mac = neighutil.get_hwaddr(peer[0])
mac = await neighutil.get_hwaddr(peer[0])
if not mac:
continue
_process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
for mac in newmacs:
for mac in list(newmacs):
thehandler = machandlers.get(mac, None)
if thehandler:
thehandler(peerbymacaddress[mac])
@@ -211,8 +226,16 @@ def get_sockets():
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return net4, net6
def active_scan(handler, protocol=None):
def _relay_pkt(sock, pktq):
sock.setblocking(0)
try:
rsp, peer = sock.recvfrom(9000)
except socket.error:
return
pktq.put_nowait((sock, rsp, peer))
async def active_scan(handler, protocol=None):
net4, net6 = get_sockets()
for idx in util.list_interface_indexes():
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF,
@@ -234,31 +257,40 @@ def active_scan(handler, protocol=None):
if se.errno != 101 and se.errno != 1:
raise
deadline = util.monotonic_time() + 2
r, _, _ = select.select((net4, net6), (), (), 2)
pktq = asyncio.Queue()
cloop = asyncio.get_running_loop()
cloop.add_reader(net4, _relay_pkt, net4, pktq)
cloop.add_reader(net6, _relay_pkt, net6, pktq)
peerdata = {}
deferparse = []
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
if not neighutil.get_hwaddr(peer[0]):
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.sendto(b'\x00', probepeer)
except Exception:
continue
deferparse.append((rsp, peer))
srp = True
timeout = 2
while timeout and srp and len(deferparse) < 256:
try:
srp = await asyncio.wait_for(pktq.get(), timeout)
except asyncio.exceptions.TimeoutError:
break
s, rsp, peer = srp
if not await neighutil.get_hwaddr(peer[0]):
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.setblocking(1)
s.sendto(b'\x00', probepeer)
except Exception:
srp = True
continue
_parse_mdns(peer, rsp, peerdata, '_obmc_console._tcp.local')
deferparse.append((rsp, peer))
srp = True
continue
await _parse_mdns(peer, rsp, peerdata, '_obmc_console._tcp.local')
timeout = deadline - util.monotonic_time()
if timeout < 0:
timeout = 0
r, _, _ = select.select((net4, net6), (), (), timeout)
if deferparse:
eventlet.sleep(2.2)
await asyncio.sleep(2.2)
for dp in deferparse:
rsp, peer = dp
_parse_mdns(peer, rsp, peerdata, '_obmc_console._tcp.local')
querypool = gp.GreenPool()
await _parse_mdns(peer, rsp, peerdata, '_obmc_console._tcp.local')
pooltargs = []
for nid in peerdata:
if '/redfish/v1/' not in peerdata[nid].get('urls', ()) and '/redfish/v1' not in peerdata[nid].get('urls', ()):
@@ -271,17 +303,24 @@ def active_scan(handler, protocol=None):
# or we drop support for those devices
#else:
# pooltargs.append(('/redfish/v1/', peerdata[nid]))
for pi in querypool.imap(check_fish, pooltargs):
if pi is not None:
handler(pi)
tsks = []
for targ in pooltargs:
tsks.append(tasks.spawn_task(check_fish(targ)))
while tsks:
done, tsks = await asyncio.wait(tsks, return_when=asyncio.FIRST_COMPLETED)
for dt in done:
dt = await dt
if dt is None:
continue
handler(dt)
def check_fish(urldata, port=443, verifycallback=None):
async def check_fish(urldata, port=443, verifycallback=None):
if not verifycallback:
verifycallback = lambda x: True
url, data = urldata
try:
wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5)
peerinfo = wc.grab_json_response(url)
wc = webclient.WebConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5)
peerinfo = await wc.grab_json_response(url)
except socket.error:
return None
if url == '/DeviceDescription.json':
@@ -370,14 +409,14 @@ def _mdns_to_dict(rsp):
return retval
def _parse_mdns(peer, rsp, peerdata, srvname):
async def _parse_mdns(peer, rsp, peerdata, srvname):
parsed = _mdns_to_dict(rsp)
if not parsed:
return
if parsed.get('ttl', 0) == 0:
return
nid = peer[0]
mac = neighutil.get_hwaddr(peer[0])
mac = await neighutil.get_hwaddr(peer[0])
if mac:
nid = mac
if nid in peerdata:
@@ -396,10 +435,10 @@ def _parse_mdns(peer, rsp, peerdata, srvname):
peerdata[nid] = peerdatum
def _parse_ssdp(peer, rsp, peerdata):
async def _parse_ssdp(peer, rsp, peerdata):
nid = peer[0]
mac = None
mac = neighutil.get_hwaddr(peer[0])
mac = await neighutil.get_hwaddr(peer[0])
if mac:
nid = mac
headlines = rsp.split(b'\r\n')
@@ -22,6 +22,7 @@
# option 97 = UUID (wireformat)
import asyncio
import base64
import confluent.config.conf as inifile
import confluent.config.configmanager as cfm
@@ -31,21 +32,20 @@ import confluent.neighutil as neighutil
import confluent.log as log
import confluent.netutil as netutil
import confluent.util as util
import confluent.tasks as tasks
import ctypes
import ctypes.util
import eventlet
import eventlet.green.socket as socket
import eventlet.green.select as select
try:
import psutil
except ImportError:
psutil = None
import netifaces
import os
import socket
import struct
import time
import traceback
import uuid
import confluent.tasks as tasks
libc = ctypes.CDLL(ctypes.util.find_library('c'))
@@ -82,47 +82,13 @@ class sockaddr_ll(ctypes.Structure):
('sll_halen', ctypes.c_ubyte),
('sll_addr', ctypes.c_ubyte * 20)]
class iovec(ctypes.Structure): # from uio.h
_fields_ = [('iov_base', ctypes.c_void_p),
('iov_len', ctypes.c_size_t)]
class msghdr(ctypes.Structure): # from bits/socket.h
_fields_ = [('msg_name', ctypes.c_void_p),
('msg_namelen', ctypes.c_uint),
('msg_iov', ctypes.POINTER(iovec)),
('msg_iovlen', ctypes.c_size_t),
('msg_control', ctypes.c_void_p),
('msg_controllen', ctypes.c_size_t),
('msg_flags', ctypes.c_int)]
class cmsghdr(ctypes.Structure): # also from bits/socket.h
_fields_ = [('cmsg_len', ctypes.c_size_t),
('cmsg_level', ctypes.c_int),
('cmsg_type', ctypes.c_int)]
# ignore the __extension__
class in_addr(ctypes.Structure):
_fields_ = [('s_addr', ctypes.c_uint32)]
class in_pktinfo(ctypes.Structure): # from bits/in.h
_fields_ = [('ipi_ifindex', ctypes.c_int),
('ipi_spec_dst', in_addr),
('ipi_addr', in_addr)]
class sockaddr_in(ctypes.Structure):
_fields_ = [('sin_family', ctypes.c_ushort), # per bits/sockaddr.h
('sin_port', ctypes.c_uint16), # per netinet/in.h
('sin_addr', in_addr)]
sendto = libc.sendto
sendto.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t,
ctypes.c_int, ctypes.POINTER(sockaddr_ll),
ctypes.c_size_t]
sendto.restype = ctypes.c_size_t
recvmsg = libc.recvmsg
recvmsg.argtypes = [ctypes.c_int, ctypes.POINTER(msghdr), ctypes.c_int]
recvmsg.restype = ctypes.c_size_t
pkttype = ctypes.c_char * 2048
@@ -154,20 +120,6 @@ def get_bcastaddr(idx):
IP_PKTINFO = 8
def CMSG_ALIGN(length): # bits/socket.h
ret = (length + ctypes.sizeof(ctypes.c_size_t) - 1
& ~(ctypes.sizeof(ctypes.c_size_t) - 1))
return ctypes.c_size_t(ret)
def CMSG_SPACE(length): # bits/socket.h
ret = CMSG_ALIGN(length).value + CMSG_ALIGN(ctypes.sizeof(cmsghdr)).value
return ctypes.c_size_t(ret)
cmsgtype = ctypes.c_char * CMSG_SPACE(ctypes.sizeof(in_pktinfo)).value
cmsgsize = CMSG_SPACE(ctypes.sizeof(in_pktinfo)).value
pxearchs = {
b'\x00\x00': 'bios-x86',
b'\x00\x07': 'uefi-x64',
@@ -301,58 +253,49 @@ def opts_to_dict(rq, optidx, expectype=1):
def ipfromint(numb):
return socket.inet_ntoa(struct.pack('I', numb))
def proxydhcp(handler, nodeguess):
def relay_proxydhcp(sock, pktq):
sock.setblocking(0)
data, cmsgs, flags, peer = sock.recvmsg(9000, 9000)
if len(data) < 240:
return
try:
optidx = data.index(b'\x63\x82\x53\x63') + 4
except ValueError:
return
for cmsg in cmsgs:
level, typ, cdata = cmsg
if level == socket.IPPROTO_IP and typ == IP_PKTINFO:
idx, recv = struct.unpack('II', cdata[:8])
recv = ipfromint(recv)
break
else:
return
rq = memoryview(data)
hwlen = rq[2]
opts, disco = opts_to_dict(rq, optidx, 3)
disco['hwaddr'] = ':'.join(['{0:02x}'.format(x) for x in rq[28:28+hwlen]])
node = None
if disco.get('hwaddr', None) in macmap:
node = macmap[disco['hwaddr']]
elif disco.get('uuid', None) in uuidmap:
node = uuidmap[disco['uuid']]
myipn = myipbypeer.get(data[28:28+hwlen], None)
skiplogging = True
pktq.put_nowait((disco, peer, myipn, idx, recv, node, opts, data))
async def proxydhcp(handler, nodeguess):
net4011 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net4011.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4011.setsockopt(socket.IPPROTO_IP, IP_PKTINFO, 1)
net4011.bind(('', 4011))
rp = bytearray(300)
rpv = memoryview(rp)
rq = bytearray(2048)
data = pkttype.from_buffer(rq)
msg = msghdr()
cmsgarr = bytearray(cmsgsize)
cmsg = cmsgtype.from_buffer(cmsgarr)
iov = iovec()
iov.iov_base = ctypes.addressof(data)
iov.iov_len = 2048
msg.msg_iov = ctypes.pointer(iov)
msg.msg_iovlen = 1
msg.msg_control = ctypes.addressof(cmsg)
msg.msg_controllen = ctypes.sizeof(cmsg)
clientaddr = sockaddr_in()
msg.msg_name = ctypes.addressof(clientaddr)
msg.msg_namelen = ctypes.sizeof(clientaddr)
cloop = asyncio.get_running_loop()
pktq = asyncio.Queue()
cloop.add_reader(net4011, relay_proxydhcp, net4011, pktq)
cfg = cfm.ConfigManager(None)
while True:
try:
ready = select.select([net4011], [], [], None)
if not ready or not ready[0]:
continue
i = recvmsg(net4011.fileno(), ctypes.pointer(msg), 0)
#nb, client = net4011.recvfrom_into(rq)
if i < 240:
continue
rqv = memoryview(rq)[:i]
client = (ipfromint(clientaddr.sin_addr.s_addr), socket.htons(clientaddr.sin_port))
_, level, typ = struct.unpack('QII', cmsgarr[:16])
if level == socket.IPPROTO_IP and typ == IP_PKTINFO:
idx, recv = struct.unpack('II', cmsgarr[16:24])
recv = ipfromint(recv)
try:
optidx = rqv.tobytes().index(b'\x63\x82\x53\x63') + 4
except ValueError:
continue
hwlen = rqv[2]
opts, disco = opts_to_dict(rqv, optidx, 3)
disco['hwaddr'] = ':'.join(['{0:02x}'.format(x) for x in rqv[28:28+hwlen]])
node = None
if disco.get('hwaddr', None) in macmap:
node = macmap[disco['hwaddr']]
elif disco.get('uuid', None) in uuidmap:
node = uuidmap[disco['uuid']]
myipn = myipbypeer.get(rqv[28:28+hwlen].tobytes(), None)
skiplogging = True
disco, client, myipn, idx, recv, node, opts, data = await pktq.get()
netaddr = disco['hwaddr']
if time.time() > ignoredisco.get(netaddr, 0) + 90:
skiplogging = False
@@ -406,6 +349,9 @@ def proxydhcp(handler, nodeguess):
'for this boot method.'.format(
node, profile, len(bootfile) - 127)})
continue
rp = bytearray(300)
rpv = memoryview(rp)
rqv = memoryview(data)
rpv[:240] = rqv[:240].tobytes()
rpv[0:1] = b'\x02'
rpv[108:108 + len(bootfile)] = bootfile
@@ -420,12 +366,38 @@ def proxydhcp(handler, nodeguess):
# tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
# event=log.Events.stacktrace)
ignorenics = None
def start_proxydhcp(handler, nodeguess=None):
eventlet.spawn_n(proxydhcp, handler, nodeguess)
tasks.spawn(proxydhcp(handler, nodeguess))
ignorenics = None
def snoop(handler, protocol=None, nodeguess=None):
def new_dhcp_packet(handler, nodeguess, cfg, net4):
data, cmsgs, flags, client = net4.recvmsg(9000, 9000)
if len(data) < 64:
return
for cmsg in cmsgs:
level, typ, cdata = cmsg
if level == socket.IPPROTO_IP and typ == IP_PKTINFO:
idx, recv = struct.unpack('II', cdata[:8])
recv = ipfromint(recv)
rqv = memoryview(data)
if rqv[0] == 1:
tasks.spawn(process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client))
def new_dhcp6_packet(handler, net6, cfg, nodeguess):
recv = 'ff02::1:2'
pkt, addr = net6.recvfrom(2048)
idx = addr[-1]
if len(pkt) < 64:
return
rqv = memoryview(pkt)
if rqv[0] in (1, 3):
tasks.spawn(process_dhcp6req(handler, rqv, addr, net6, cfg, nodeguess))
async def snoop(handler, protocol=None, nodeguess=None):
global ignorenics
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
#prominent
@@ -437,6 +409,7 @@ def snoop(handler, protocol=None, nodeguess=None):
ignorenics = ignorenics.encode()
ignorenics = ignorenics.split(b',')
start_proxydhcp(handler, nodeguess)
global tracelog
tracelog = log.Logger('trace')
global attribwatcher
cfg = cfm.ConfigManager(None)
@@ -459,78 +432,18 @@ def snoop(handler, protocol=None, nodeguess=None):
v6grp = v6addr + struct.pack('=I', ifidx)
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, v6grp)
net6.bind(('', 547))
clientaddr = sockaddr_in()
rawbuffer = bytearray(2048)
data = pkttype.from_buffer(rawbuffer)
msg = msghdr()
cmsgarr = bytearray(cmsgsize)
cmsg = cmsgtype.from_buffer(cmsgarr)
iov = iovec()
iov.iov_base = ctypes.addressof(data)
iov.iov_len = 2048
msg.msg_iov = ctypes.pointer(iov)
msg.msg_iovlen = 1
msg.msg_control = ctypes.addressof(cmsg)
msg.msg_controllen = ctypes.sizeof(cmsg)
msg.msg_name = ctypes.addressof(clientaddr)
msg.msg_namelen = ctypes.sizeof(clientaddr)
# We'll leave name and namelen blank for now
while True:
try:
# Just need some delay, picked a prime number so that overlap with other
# timers might be reduced, though it really is probably nothing
ready = select.select([net4, net6], [], [], 1)
for txid in list(_recent_txids):
if _recent_txids[txid] < time.time():
del _recent_txids[txid]
if not ready or not ready[0]:
continue
for netc in ready[0]:
idx = None
if netc == net4:
i = recvmsg(netc.fileno(), ctypes.pointer(msg), 0)
# if we have a small packet, just skip, it can't possible hold enough
# data and avoids some downstream IndexErrors that would be messy
# with try/except
if i < 64:
continue
if rawbuffer[0] == 1: # Boot request
_, level, typ = struct.unpack('QII', cmsgarr[:16])
if level == socket.IPPROTO_IP and typ == IP_PKTINFO:
idx, recv = struct.unpack('II', cmsgarr[16:24])
if ignorenics:
ignore = False
for nic in ignorenics:
if libc.if_nametoindex(nic) == idx:
ignore = True
break # ignore DHCP from ignored NIC
if ignore:
continue
recv = ipfromint(recv)
rqv = memoryview(rawbuffer)[:i]
client = (ipfromint(clientaddr.sin_addr.s_addr), socket.htons(clientaddr.sin_port))
process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client)
elif netc == net6:
recv = 'ff02::1:2'
pkt, addr = netc.recvfrom(2048)
idx = addr[-1]
i = len(pkt)
if i < 64:
continue
rqv = memoryview(pkt)
rq = bytearray(rqv[:2])
if rq[0] in (1, 3): # dhcpv6 solicit
process_dhcp6req(handler, rqv, addr, netc, cfg, nodeguess)
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
net6.settimeout(0)
net4.settimeout(0)
cloop = asyncio.get_running_loop()
# TODO:asyncmerge: honor ignorenics, clean the _recent_txids that have expired
cloop.add_reader(net4, new_dhcp_packet, handler, nodeguess, cfg, net4)
cloop.add_reader(net6, new_dhcp6_packet, handler, net6, cfg, nodeguess)
_mac_to_uuidmap = {}
def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess):
async def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess):
ip = addr[0]
req, disco = v6opts_to_dict(bytearray(rqv[4:]))
req['txid'] = rqv[1:4]
@@ -540,22 +453,22 @@ def process_dhcp6req(handler, rqv, addr, net, cfg, nodeguess):
if disco['uuid'] == '03000200-0400-0500-0006-000700080009':
# Ignore common malformed dhcpv6 request from firmware
return
mac = neighutil.get_hwaddr(ip.split('%', 1)[0])
mac = await neighutil.get_hwaddr(ip.split('%', 1)[0])
if not mac:
net.sendto(b'\x00', addr)
tries = 5
while tries and not mac:
eventlet.sleep(0.01)
await asyncio.sleep(0.01)
tries -= 1
mac = neighutil.get_hwaddr(ip.split('%', 1)[0])
mac = await neighutil.get_hwaddr(ip.split('%', 1)[0])
info = {'hwaddr': mac, 'uuid': disco['uuid'],
'architecture': disco['arch'], 'services': ('pxe-client',)}
if ignoredisco.get(mac, 0) + 90 < time.time():
ignoredisco[mac] = time.time()
handler(info)
consider_discover(info, req, net, cfg, None, nodeguess, addr)
await consider_discover(info, req, net, cfg, None, nodeguess, addr)
def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client):
async def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client):
rq = bytearray(rqv)
addrlen = rq[2]
if addrlen > 16 or addrlen == 0:
@@ -598,7 +511,7 @@ def process_dhcp4req(handler, nodeguess, cfg, net4, idx, recv, rqv, client):
and time.time() > ignoredisco.get(netaddr, 0) + 90):
ignoredisco[netaddr] = time.time()
handler(info)
consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess, requestor=client)
await consider_discover(info, rqinfo, net4, cfg, rqv, nodeguess, requestor=client)
@@ -656,7 +569,7 @@ def get_deployment_profile(node, cfg, cfd=None):
staticassigns = {}
myipbypeer = {}
def check_reply(node, info, packet, sock, cfg, reqview, addr, requestor):
async def check_reply(node, info, packet, sock, cfg, reqview, addr, requestor):
if not requestor:
requestor = ('0.0.0.0', None)
if requestor[0] == '0.0.0.0' and not info.get('uuid', None):
@@ -677,16 +590,16 @@ def check_reply(node, info, packet, sock, cfg, reqview, addr, requestor):
if packet['vci'] and packet['vci'].startswith('PXEClient'):
log.log({'info': 'IPv6 PXE boot attempt by {0}, but IPv6 PXE is not supported, try IPv6 HTTP boot or IPv4 boot'.format(node)})
return
return reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock)
return await reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock)
else:
return reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock, requestor)
return await reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock, requestor)
def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock):
myaddrs = netutil.get_my_addresses(addr[-1], socket.AF_INET6)
async def reply_dhcp6(node, addr, cfg, packet, cfd, profile, sock):
myaddrs = await netutil.get_my_addresses(addr[-1], socket.AF_INET6)
if not myaddrs:
log.log({'info': 'Unable to provide IPv6 boot services to {0}, no viable IPv6 configuration on interface index "{1}" to respond through.'.format(node, addr[-1])})
return
niccfg = netutil.get_nic_config(cfg, node, ifidx=addr[-1], onlyfamily=socket.AF_INET6)
niccfg = await netutil.get_nic_config(cfg, node, ifidx=addr[-1], onlyfamily=socket.AF_INET6)
ipv6addr = niccfg.get('ipv6_address', None)
ipv6prefix = niccfg.get('ipv6_prefix', None)
ipv6method = niccfg.get('ipv6_method', 'static')
@@ -766,7 +679,7 @@ def get_my_duid():
_recent_txids = {}
def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=None, requestor=None):
async def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=None, requestor=None):
replen = 275 # default is going to be 286
# while myipn is describing presumed destination, it's really
# vague in the face of aliases, need to convert to ifidx and evaluate
@@ -811,7 +724,7 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N
relayipa = socket.inet_ntoa(relayip)
gateway = None
netmask = None
niccfg = netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayipn=relayip, onlyfamily=socket.AF_INET)
niccfg = await netutil.get_nic_config(cfg, node, ifidx=info['netinfo']['ifidx'], relayipn=relayip, onlyfamily=socket.AF_INET)
nicerr = niccfg.get('error_msg', False)
if nicerr:
log.log({'error': nicerr})
@@ -958,12 +871,12 @@ def reply_dhcp4(node, info, packet, cfg, reqview, httpboot, cfd, profile, sock=N
ipinfo = 'without address, served from {0}'.format(myip)
if relayipa:
ipinfo += ' (relayed to {} via {})'.format(relayipa, requestor[0])
eventlet.spawn(send_rsp, repview, replen, requestor, relayip, reqview, info, deferanswer, isboot, node, boottype, ipinfo, sock)
tasks.spawn(send_rsp(repview, replen, requestor, relayip, reqview, info, deferanswer, isboot, node, boottype, ipinfo, sock))
def send_rsp(repview, replen, requestor, relayip, reqview, info, defertxid, isboot, node, boottype, ipinfo, sock):
async def send_rsp(repview, replen, requestor, relayip, reqview, info, defertxid, isboot, node, boottype, ipinfo, sock):
if defertxid:
eventlet.sleep(0.5)
await asyncio.sleep(0.5)
if defertxid in _recent_txids:
log.log({'info': 'Skipping reply for {} over interface {} due to better offer being made over other interface'.format(node, info['netinfo']['ifidx'])})
return
@@ -1029,13 +942,13 @@ def ack_request(pkt, rq, info, sock=None, requestor=None):
else:
send_raw_packet(repview, len(rply), rq, info)
def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None, requestor=None):
async def consider_discover(info, packet, sock, cfg, reqview, nodeguess, addr=None, requestor=None):
if packet.get(53, None) == b'\x03':
ack_request(packet, reqview, info, sock, requestor)
elif info.get('hwaddr', None) in macmap: # and info.get('uuid', None):
check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr, requestor)
await check_reply(macmap[info['hwaddr']], info, packet, sock, cfg, reqview, addr, requestor)
elif info.get('uuid', None) in uuidmap:
check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr, requestor)
await check_reply(uuidmap[info['uuid']], info, packet, sock, cfg, reqview, addr, requestor)
elif packet.get(53, None) == b'\x03':
ack_request(packet, reqview, info, sock, requestor)
elif info.get('uuid', None) and info.get('hwaddr', None):
@@ -14,14 +14,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import confluent.neighutil as neighutil
import confluent.tasks as tasks
import confluent.util as util
import confluent.log as log
import os
import random
import eventlet.greenpool
import eventlet.green.select as select
import eventlet.green.socket as socket
import socket
import struct
import traceback
@@ -102,12 +102,12 @@ def _parse_SrvRply(parsed):
parsed['urls'].append(url)
def _parse_slp_packet(packet, peer, rsps, xidmap, defer=None, sock=None):
async def _parse_slp_packet(packet, peer, rsps, xidmap, defer=None, sock=None):
parsed = _parse_slp_header(packet)
if not parsed:
return
addr = peer[0]
mac = neighutil.get_hwaddr(addr)
mac = await neighutil.get_hwaddr(addr)
if mac:
identifier = mac
else:
@@ -116,6 +116,7 @@ def _parse_slp_packet(packet, peer, rsps, xidmap, defer=None, sock=None):
else:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
sock.setblocking(1)
sock.sendto(b'\x00', probepeer)
except Exception:
return
@@ -198,7 +199,7 @@ def _generate_request_payload(srvtype, multicast, xid, prlist=''):
return header + payload
def _find_srvtype(net, net4, srvtype, addresses, xid):
async def _find_srvtype(net, net4, srvtype, addresses, xid):
"""Internal function to find a single service type
Helper to do singleton requests to srvtype
@@ -208,10 +209,11 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
:param addresses: Pass through of addresses argument from find_targets
:return:
"""
cloop = asyncio.get_running_loop()
data = _generate_request_payload(srvtype, True, xid)
if addresses is not None:
for addr in addresses:
for saddr in socket.getaddrinfo(addr, 427):
for saddr in await cloop.getaddrinfo(addr, 427):
if saddr[0] == socket.AF_INET:
net4.sendto(data, saddr[4])
elif saddr[0] == socket.AF_INET6:
@@ -253,21 +255,16 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
pass
def _grab_rsps(socks, rsps, interval, xidmap, deferrals):
r = None
res = select.select(socks, (), (), interval)
if res:
r = res[0]
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
_parse_slp_packet(rsp, peer, rsps, xidmap, deferrals, s)
res = select.select(socks, (), (), interval)
if not res:
r = None
else:
r = res[0]
import time
def sock_read(fut, sock, cloop, allsocks):
if fut.done():
print("was already done???")
return
if not cloop.remove_reader(sock):
print("Was already removed??")
fut.set_result(sock)
allsocks.discard(sock)
def _parse_attrlist(attrstr):
@@ -335,16 +332,17 @@ def _parse_attrs(data, parsed, xid=None):
parsed['attributes'] = _parse_attrlist(attrstr)
def fix_info(info, handler):
async def fix_info(info, handler):
if '_attempts' not in info:
info['_attempts'] = 10
if info['_attempts'] == 0:
return
info['_attempts'] -= 1
_add_attributes(info)
await _add_attributes(info)
handler(info)
def _add_attributes(parsed):
async def _add_attributes(parsed):
xid = parsed.get('xid', 42)
attrq = _generate_attr_request(parsed['services'][0], xid)
target = None
@@ -360,14 +358,16 @@ def _add_attributes(parsed):
net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
cloop = asyncio.get_running_loop()
try:
net.settimeout(2.0)
net.connect(target)
except socket.error:
net.settimeout(0)
net.setblocking(0)
await asyncio.wait_for(cloop.sock_connect(net, target), 2.0)
except (socket.error, asyncio.exceptions.TimeoutError) as te:
return
try:
net.sendall(attrq)
rsp = net.recv(8192)
await cloop.sock_sendall(net, attrq)
rsp = await cloop.sock_recv(net, 8192)
net.close()
_parse_attrs(rsp, parsed, xid)
except Exception as e:
@@ -379,11 +379,12 @@ def _add_attributes(parsed):
def unicast_scan(address):
pass
def query_srvtypes(target):
async def query_srvtypes(target):
"""Query the srvtypes advertised by the target
:param target: A sockaddr tuple (if you get the peer info)
"""
cloop = asyncio.get_running_loop()
payload = b'\x00\x00\xff\xff\x00\x07DEFAULT'
header = _generate_slp_header(payload, False, functionid=9, xid=1)
packet = header + payload
@@ -398,15 +399,13 @@ def query_srvtypes(target):
while tries and not connected:
tries -= 1
try:
net.settimeout(1.0)
net.connect(target)
net.settimeout(0)
await asyncio.wait_for(cloop.sock_connect(net, target), 2.0)
connected = True
except socket.error:
pass
if not connected:
return [u'']
net.sendall(packet)
rs = net.recv(8192)
except (socket.error, asyncio.exceptions.TimeoutError) as te:
return [u'']
await cloop.sock_sendall(net, packet)
rs = await cloop.sock_recv(net, 8192)
net.close()
parsed = _parse_slp_header(rs)
if parsed:
@@ -417,13 +416,13 @@ def query_srvtypes(target):
stypes = payload[4:4+stypelen].decode('utf-8')
return stypes.split(',')
def rescan(handler):
async def rescan(handler):
known_peers = set([])
for scanned in scan():
async for scanned in scan():
for addr in scanned['addresses']:
if addr in known_peers:
break
macaddr = neighutil.get_hwaddr(addr[0])
macaddr = await neighutil.get_hwaddr(addr[0])
if not macaddr:
continue
known_peers.add(addr)
@@ -431,7 +430,15 @@ def rescan(handler):
handler(scanned)
def snoop(handler, protocol=None):
def relay_packet(sock, pktq):
sock.setblocking(0)
try:
rsp, peer = sock.recvfrom(9000)
except socket.error as se:
return
pktq.put_nowait((sock, rsp, peer))
async def snoop(handler, protocol=None):
"""Watch for SLP activity
handler will be called with a dictionary of relevant attributes
@@ -441,10 +448,10 @@ def snoop(handler, protocol=None):
"""
tracelog = log.Logger('trace')
try:
active_scan(handler, protocol)
await active_scan(handler, protocol)
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
slpg = socket.inet_pton(socket.AF_INET6, 'ff01::123')
@@ -471,6 +478,10 @@ def snoop(handler, protocol=None):
# socket in use can occur when aliased ipv4 are encountered
net.bind(('', 427))
net4.bind(('', 427))
pktq = asyncio.Queue()
cloop = asyncio.get_running_loop()
cloop.add_reader(net, relay_packet, net, pktq)
cloop.add_reader(net4, relay_packet, net4, pktq)
newmacs = set([])
known_peers = set([])
peerbymacaddress = {}
@@ -478,7 +489,6 @@ def snoop(handler, protocol=None):
while True:
try:
newmacs.clear()
r, _, _ = select.select((net, net4), (), (), 60)
# clear known_peers and peerbymacaddress
# to avoid stale info getting in...
# rely upon the select(0.2) to catch rapid fire and aggregate ip
@@ -488,31 +498,37 @@ def snoop(handler, protocol=None):
known_peers.clear()
peerbymacaddress.clear()
deferpeers.clear()
while r and len(deferpeers) < 256:
for s in r:
(rsp, peer) = s.recvfrom(9000)
if peer in known_peers:
timeo = 60
rdy = True
srp = await pktq.get()
while srp and len(deferpeers) < 256:
s, rsp, peer = srp
try:
srp = await asyncio.wait_for(pktq.get(), 0.2)
except asyncio.exceptions.TimeoutError:
srp = None
if peer in known_peers:
continue
if peer in deferpeers:
continue
mac = await neighutil.get_hwaddr(peer[0])
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.setblocking(1)
s.sendto(b'\x00', probepeer)
except Exception as e:
continue
if peer in deferpeers:
continue
mac = neighutil.get_hwaddr(peer[0])
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.sendto(b'\x00', probepeer)
except Exception:
continue
deferpeers.append(peer)
continue
process_peer(newmacs, known_peers, peerbymacaddress, peer)
r, _, _ = select.select((net, net4), (), (), 0.2)
deferpeers.append(peer)
continue
await process_peer(newmacs, known_peers, peerbymacaddress, peer)
if deferpeers:
eventlet.sleep(2.2)
await asyncio.sleep(2.2)
for peer in deferpeers:
process_peer(newmacs, known_peers, peerbymacaddress, peer)
await process_peer(newmacs, known_peers, peerbymacaddress, peer)
for mac in newmacs:
peerbymacaddress[mac]['xid'] = 1
_add_attributes(peerbymacaddress[mac])
await _add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
peerbymacaddress[mac]['protocol'] = protocol
for srvurl in peerbymacaddress[mac].get('urls', ()):
@@ -534,8 +550,8 @@ def snoop(handler, protocol=None):
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def process_peer(newmacs, known_peers, peerbymacaddress, peer):
mac = neighutil.get_hwaddr(peer[0])
async def process_peer(newmacs, known_peers, peerbymacaddress, peer):
mac = await neighutil.get_hwaddr(peer[0])
if not mac:
return
known_peers.add(peer)
@@ -543,7 +559,7 @@ def process_peer(newmacs, known_peers, peerbymacaddress, peer):
peerbymacaddress[mac]['addresses'].append(peer)
else:
try:
q = query_srvtypes(peer)
q = await query_srvtypes(peer)
except Exception as e:
q = None
if not q or not q[0]:
@@ -565,17 +581,16 @@ def process_peer(newmacs, known_peers, peerbymacaddress, peer):
newmacs.add(mac)
def active_scan(handler, protocol=None):
async def active_scan(handler, protocol=None):
known_peers = set([])
toprocess = []
# Implement a warmup, inducing neighbor table activity
# by kernel and giving 2 seconds for a retry or two if
# needed
for scanned in scan():
async for scanned in scan():
for addr in scanned['addresses']:
if addr in known_peers:
break
macaddr = neighutil.get_hwaddr(addr[0])
macaddr = await neighutil.get_hwaddr(addr[0])
if not macaddr:
continue
if not scanned.get('hwaddr', None):
@@ -586,7 +601,7 @@ def active_scan(handler, protocol=None):
handler(scanned)
def scan(srvtypes=_slp_services, addresses=None, localonly=False):
async def scan(srvtypes=_slp_services, addresses=None, localonly=False):
"""Find targets providing matching requested srvtypes
This is a generator that will iterate over respondants to the SrvType
@@ -611,6 +626,10 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
# too, so force it
#net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
# we are going to do broadcast, so allow that...
cloop = asyncio.get_running_loop()
pktq = asyncio.Queue()
cloop.add_reader(net, relay_packet, net, pktq)
cloop.add_reader(net4, relay_packet, net4, pktq)
initxid = random.randint(0, 32768)
xididx = 0
xidmap = {}
@@ -618,23 +637,29 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
# processed, mitigating volume of response traffic
rsps = {}
deferrals = []
rcvq = asyncio.Queue()
for srvtype in srvtypes:
xididx += 1
_find_srvtype(net, net4, srvtype, addresses, initxid + xididx)
await _find_srvtype(net, net4, srvtype, addresses, initxid + xididx)
xidmap[initxid + xididx] = srvtype
_grab_rsps((net, net4), rsps, 0.1, xidmap, deferrals)
# now do a more slow check to work to get stragglers,
# but fortunately the above should have taken the brunt of volume, so
# reduced chance of many responses overwhelming receive buffer.
_grab_rsps((net, net4), rsps, 1, xidmap, deferrals)
await asyncio.sleep(0) # give async a chance to move things off buffer to queue
while True:
try:
srp = await asyncio.wait_for(pktq.get(), 1.0)
sock, rsp, peer = srp
await _parse_slp_packet(rsp, peer, rsps, xidmap, deferrals, sock)
except asyncio.exceptions.TimeoutError:
break
cloop.remove_reader(net)
cloop.remove_reader(net4)
if deferrals:
eventlet.sleep(1.2) # already have a one second pause from select above
await asyncio.sleep(1.2) # already have a one second pause from select above
for defer in deferrals:
rsp, peer = defer
_parse_slp_packet(rsp, peer, rsps, xidmap)
await _parse_slp_packet(rsp, peer, rsps, xidmap)
# now to analyze and flesh out the responses
handleids = set([])
gp = eventlet.greenpool.GreenPool(128)
tsks = []
for id in rsps:
for srvurl in rsps[id].get('urls', ()):
if len(srvurl) > 4:
@@ -649,9 +674,10 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
break
else:
continue
gp.spawn_n(_add_attributes, rsps[id])
tsks.append(tasks.spawn_task(_add_attributes(rsps[id])))
handleids.add(id)
gp.waitall()
if tsks:
await asyncio.wait(tsks)
for id in handleids:
if 'service:lighttpd' in rsps[id]['services']:
currinf = rsps[id]
@@ -28,6 +28,7 @@
# NTS: ssdp:alive
import asyncio
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
import confluent.neighutil as neighutil
@@ -35,16 +36,14 @@ import confluent.noderange as noderange
import confluent.util as util
import confluent.log as log
import confluent.netutil as netutil
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
import eventlet.greenpool as gp
import confluent.tasks as tasks
import socket
import os
import time
import struct
import traceback
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
mcastv4addr = '239.255.255.250'
mcastv6addr = 'ff02::c'
@@ -56,14 +55,14 @@ smsg = ('M-SEARCH * HTTP/1.1\r\n'
'MX: 3\r\n\r\n')
def active_scan(handler, protocol=None):
async def active_scan(handler, protocol=None):
known_peers = set([])
for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::dmtf-org:service:redfish-rest:', 'urn::service:affluent']):
async for scanned in scan(['urn:dmtf-org:service:redfish-rest:1', 'urn::dmtf-org:service:redfish-rest:', 'urn::service:affluent']):
for addr in scanned['addresses']:
addr = addr[0:1] + addr[2:]
if addr in known_peers:
break
hwaddr = neighutil.get_hwaddr(addr[0])
hwaddr = await neighutil.get_hwaddr(addr[0])
if not hwaddr:
continue
if not scanned.get('hwaddr', None):
@@ -73,9 +72,9 @@ def active_scan(handler, protocol=None):
scanned['protocol'] = protocol
handler(scanned)
def scan(services, target=None):
async def scan(services, target=None):
for service in services:
for rply in _find_service(service, target):
async for rply in _find_service(service, target):
yield rply
@@ -116,18 +115,17 @@ def _process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byeha
if '/eth' in value and value.endswith('.xml'):
targurl = '/redfish/v1/'
targtype = 'megarac-bmc'
continue # MegaRAC redfish
continue # MegaRAC redfish
elif value.endswith('/DeviceDescription.json'):
targurl = '/DeviceDescription.json'
targtype = 'lenovo-xcc'
continue
else:
return
if handler and targurl:
eventlet.spawn_n(check_fish_handler, handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype)
if handler:
tasks.spawn(check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype))
def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype):
retdata = check_fish((targurl, peerdata, targtype))
async def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress, machandlers, mac, peer, targurl, targtype):
retdata = await check_fish(('/DeviceDescription.json', peerdata, targtype))
if retdata:
known_peers.add(peer)
newmacs.add(mac)
@@ -135,7 +133,7 @@ def check_fish_handler(handler, peerdata, known_peers, newmacs, peerbymacaddress
machandlers[mac] = handler
def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
async def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
"""Watch for SSDP notify messages
The handler shall be called on any service coming online.
@@ -152,8 +150,9 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
# dabbling in multicast wizardry here, such sockets can cause big problems,
# so we will have two distinct sockets
tracelog = log.Logger('trace')
cloop = asyncio.get_running_loop()
try:
active_scan(handler, protocol)
await active_scan(handler, protocol)
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
@@ -179,6 +178,10 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.bind(('', 1900))
net6.bind(('', 1900))
pktq = asyncio.Queue()
cloop = asyncio.get_running_loop()
cloop.add_reader(net4, _relay_pkt, net4, pktq)
cloop.add_reader(net6, _relay_pkt, net6, pktq)
peerbymacaddress = {}
newmacs = set([])
deferrednotifies = []
@@ -188,131 +191,135 @@ def snoop(handler, byehandler=None, protocol=None, uuidlookup=None):
newmacs.clear()
deferrednotifies.clear()
machandlers.clear()
r = select.select((net4, net6), (), (), 60)
if r:
r = r[0]
recent_peers = set([])
while r and len(deferrednotifies) < 256:
for s in r:
(rsp, peer) = s.recvfrom(9000)
if rsp[:4] == b'PING':
timeout = None
srp = await pktq.get()
recent_peers.clear()
while srp and len(deferrednotifies) < 256:
srp = None
if timeout is None:
srp = await pktq.get()
else:
try:
srp = await asyncio.wait_for(pktq.get(), timeout=timeout)
except asyncio.exceptions.TimeoutError:
break
timeout = 0.2
s, rsp, peer = srp
if rsp[:4] == b'PING':
continue
if peer in recent_peers:
continue
rsp = rsp.split(b'\r\n')
if b' ' not in rsp[0]:
continue
method, _ = rsp[0].split(b' ', 1)
if method == b'NOTIFY':
if peer in known_peers:
continue
if peer in recent_peers:
recent_peers.add(peer)
mac = await neighutil.get_hwaddr(peer[0])
if mac == False:
# neighutil determined peer ip is not local, skip attempt
# to probe and critically, skip growing deferrednotifiers
continue
rsp = rsp.split(b'\r\n')
if b' ' not in rsp[0]:
continue
method, _ = rsp[0].split(b' ', 1)
if method == b'NOTIFY':
if peer in known_peers:
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.setblocking(1)
s.sendto(b'\x00', probepeer)
except Exception:
continue
recent_peers.add(peer)
mac = neighutil.get_hwaddr(peer[0])
if mac == False:
# neighutil determined peer ip is not local, skip attempt
# to probe and critically, skip growing deferrednotifiers
deferrednotifies.append((peer, rsp))
continue
_process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
elif method == b'M-SEARCH':
if not uuidlookup:
continue
#ip = peer[0].partition('%')[0]
for headline in rsp[1:]:
if not headline:
continue
if not mac:
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
headline = util.stringify(headline)
headline = headline.partition(':')
if len(headline) < 3:
continue
forcereply = False
if headline[0] == 'ST' and headline[-1].startswith(' urn:xcat.org:service:confluent:'):
try:
s.sendto(b'\x00', probepeer)
cfm.check_quorum()
except Exception:
continue
deferrednotifies.append((peer, rsp))
continue
_process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
elif method == b'M-SEARCH':
if not uuidlookup:
continue
#ip = peer[0].partition('%')[0]
for headline in rsp[1:]:
if not headline:
continue
headline = util.stringify(headline)
headline = headline.partition(':')
if len(headline) < 3:
continue
forcereply = False
if headline[0] == 'ST' and headline[-1].startswith(' urn:xcat.org:service:confluent:'):
try:
cfm.check_quorum()
except Exception:
continue
for query in headline[-1].split('/'):
node = None
if query.startswith('confluentuuid='):
myuuid = cfm.get_global('confluent_uuid')
curruuid = query.split('=', 1)[1].lower()
if curruuid != myuuid:
break
forcereply = True
elif query.startswith('allconfluent=1'):
reply = 'HTTP/1.1 200 OK\r\n\r\nCONFLUENT: PRESENT\r\n'
if not isinstance(reply, bytes):
reply = reply.encode('utf8')
try:
s.sendto(reply, peer)
except Exception:
break
elif query.startswith('uuid='):
curruuid = query.split('=', 1)[1].lower()
node = uuidlookup(curruuid)
elif query.startswith('mac='):
currmac = query.split('=', 1)[1].lower()
node = uuidlookup(currmac)
if node:
cfg = cfm.ConfigManager(None)
cfd = cfg.get_node_attributes(
node, ['deployment.pendingprofile', 'collective.managercandidates'])
if not forcereply:
# Do not bother replying to a node that
# we have no deployment activity
# planned for
if not cfd.get(node, {}).get(
'deployment.pendingprofile', {}).get('value', None):
break
candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None)
if candmgrs:
try:
candmgrs = noderange.NodeRange(candmgrs, cfg).nodes
except Exception:
candmgrs = noderange.NodeRange(candmgrs).nodes
if collective.get_myname() not in candmgrs:
break
currtime = time.time()
seconds = int(currtime)
msecs = int(currtime * 1000 % 1000)
reply = 'HTTP/1.1 200 OK\r\nNODENAME: {0}\r\nCURRTIME: {1}\r\nCURRMSECS: {2}\r\n'.format(node, seconds, msecs)
theip = peer[0].split('%', 1)[0]
if netutil.ip_on_same_subnet(theip, 'fe80::', 64):
if '%' in peer[0]:
ifidx = peer[0].split('%', 1)[1]
iface = socket.getaddrinfo(peer[0], 0, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1][-1]
else:
ifidx = '{}'.format(peer[-1])
iface = peer[-1]
reply += 'MGTIFACE: {0}\r\n'.format(ifidx)
ncfg = netutil.get_nic_config(
cfg, node, ifidx=iface)
if ncfg.get('matchesnodename', None):
reply += 'DEFAULTNET: 1\r\n'
elif not netutil.address_is_local(peer[0]):
continue
if not isinstance(reply, bytes):
reply = reply.encode('utf8')
try:
s.sendto(reply, peer)
except Exception:
pass
for query in headline[-1].split('/'):
node = None
if query.startswith('confluentuuid='):
myuuid = cfm.get_global('confluent_uuid')
curruuid = query.split('=', 1)[1].lower()
if curruuid != myuuid:
break
r = select.select((net4, net6), (), (), 0.2)
if r:
r = r[0]
forcereply = True
elif query.startswith('allconfluent=1'):
reply = 'HTTP/1.1 200 OK\r\n\r\nCONFLUENT: PRESENT\r\n'
if not isinstance(reply, bytes):
reply = reply.encode('utf8')
s.setblocking(1)
try:
s.sendto(reply, peer)
except Exception:
break
elif query.startswith('uuid='):
curruuid = query.split('=', 1)[1].lower()
node = uuidlookup(curruuid)
elif query.startswith('mac='):
currmac = query.split('=', 1)[1].lower()
node = uuidlookup(currmac)
if node:
cfg = cfm.ConfigManager(None)
cfd = cfg.get_node_attributes(
node, ['deployment.pendingprofile', 'collective.managercandidates'])
if not forcereply:
# Do not bother replying to a node that
# we have no deployment activity
# planned for
if not cfd.get(node, {}).get(
'deployment.pendingprofile', {}).get('value', None):
break
candmgrs = cfd.get(node, {}).get('collective.managercandidates', {}).get('value', None)
if candmgrs:
candmgrs = noderange.NodeRange(candmgrs, cfg).nodes
if collective.get_myname() not in candmgrs:
break
currtime = time.time()
seconds = int(currtime)
msecs = int(currtime * 1000 % 1000)
reply = 'HTTP/1.1 200 OK\r\nNODENAME: {0}\r\nCURRTIME: {1}\r\nCURRMSECS: {2}\r\n'.format(node, seconds, msecs)
theip = peer[0].split('%', 1)[0]
if await netutil.ip_on_same_subnet(theip, 'fe80::', 64):
if '%' in peer[0]:
ifidx = peer[0].split('%', 1)[1]
iface = await cloop.getaddrinfo(peer[0], 0, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1][-1]
else:
ifidx = '{}'.format(peer[-1])
iface = peer[-1]
reply += 'MGTIFACE: {0}\r\n'.format(ifidx)
ncfg = await netutil.get_nic_config(
cfg, node, ifidx=iface)
if ncfg.get('matchesnodename', None):
reply += 'DEFAULTNET: 1\r\n'
elif not await netutil.address_is_local(peer[0]):
continue
if not isinstance(reply, bytes):
reply = reply.encode('utf8')
s.setblocking(1)
try:
s.sendto(reply, peer)
except Exception:
pass
break
if deferrednotifies:
eventlet.sleep(2.2)
await asyncio.sleep(2.2)
for peerrsp in deferrednotifies:
peer, rsp = peerrsp
mac = neighutil.get_hwaddr(peer[0])
mac = await neighutil.get_hwaddr(peer[0])
if not mac:
continue
_process_snoop(peer, rsp, mac, known_peers, newmacs, peerbymacaddress, byehandler, machandlers, handler)
@@ -333,12 +340,24 @@ def _get_svrip(peerdata):
return addr[0]
return peerdata['addresses'][0][0]
def _find_service(service, target):
def _relay_pkt(sock, pktq):
sock.setblocking(0)
try:
rsp, peer = sock.recvfrom(9000)
except socket.error as se:
return
pktq.put_nowait((sock, rsp, peer))
async def _find_service(service, target):
cloop = asyncio.get_running_loop()
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
pktq = asyncio.Queue()
cloop.add_reader(net4, _relay_pkt, net4, pktq)
cloop.add_reader(net6, _relay_pkt, net6, pktq)
if target:
addrs = socket.getaddrinfo(target, 1900, 0, socket.SOCK_DGRAM)
addrs = await cloop.getaddrinfo(target, 1900, 0, socket.SOCK_DGRAM)
for addr in addrs:
host = addr[4][0]
if addr[0] == socket.AF_INET:
@@ -390,31 +409,35 @@ def _find_service(service, target):
# SSDP by spec encourages responses to spread out over a 3 second interval
# hence we must be a bit more patient
deadline = util.monotonic_time() + 4
r, _, _ = select.select((net4, net6), (), (), 4)
peerdata = {}
deferparse = []
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
if not neighutil.get_hwaddr(peer[0]):
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.sendto(b'\x00', probepeer)
except Exception:
continue
deferparse.append((rsp, peer))
srp = True
timeout = 4
while timeout and srp and len(deferparse) < 256:
try:
srp = await asyncio.wait_for(pktq.get(), timeout)
except asyncio.exceptions.TimeoutError:
break
s, rsp, peer = srp
if not await neighutil.get_hwaddr(peer[0]):
probepeer = (peer[0], struct.unpack('H', os.urandom(2))[0] | 1025) + peer[2:]
try:
s.sendto(b'\x00', probepeer)
except Exception:
srp = True
continue
_parse_ssdp(peer, rsp, peerdata)
deferparse.append((rsp, peer))
srp = True
continue
await _parse_ssdp(peer, rsp, peerdata)
timeout = deadline - util.monotonic_time()
if timeout < 0:
timeout = 0
r, _, _ = select.select((net4, net6), (), (), timeout)
if deferparse:
eventlet.sleep(2.2)
await asyncio.sleep(2.2)
for dp in deferparse:
rsp, peer = dp
_parse_ssdp(peer, rsp, peerdata)
querypool = gp.GreenPool()
await _parse_ssdp(peer, rsp, peerdata)
pooltargs = []
for nid in peerdata:
if peerdata[nid].get('services', [None])[0] == 'urn::service:affluent:1':
@@ -441,7 +464,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
@@ -450,11 +473,18 @@ def _find_service(service, target):
# or we drop support for those devices
#else:
# pooltargs.append(('/redfish/v1/', peerdata[nid]))
for pi in querypool.imap(check_fish, pooltargs):
if pi is not None:
yield pi
tsks = []
for targ in pooltargs:
tsks.append(tasks.spawn_task(check_fish(targ)))
while tsks:
done, tsks = await asyncio.wait(tsks, return_when=asyncio.FIRST_COMPLETED)
for dt in done:
dt = await dt
if dt is None:
continue
yield dt
def check_fish(urldata, port=443, verifycallback=None):
async def check_fish(urldata, port=443, verifycallback=None):
if not verifycallback:
verifycallback = lambda x: True
try:
@@ -463,8 +493,8 @@ def check_fish(urldata, port=443, verifycallback=None):
url, data = urldata
targtype = 'service:redfish-bmc'
try:
wc = webclient.SecureHTTPConnection(_get_svrip(data), port, verifycallback=verifycallback, timeout=1.5)
peerinfo = wc.grab_json_response(url, headers={'Accept': 'application/json'})
wc = webclient.WebConnection(_get_svrip(data), port, verifycallback=verifycallback)
peerinfo = await wc.grab_json_response(url, headers={'Accept': 'application/json'})
except socket.error:
return None
if url == '/DeviceDescription.json':
@@ -491,7 +521,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']
@@ -499,7 +529,7 @@ def check_fish(urldata, port=443, verifycallback=None):
return data
return None
url = '/redfish/v1/'
peerinfo = wc.grab_json_response('/redfish/v1/')
peerinfo = await wc.grab_json_response('/redfish/v1/')
if url == '/redfish/v1/':
if 'UUID' in peerinfo:
data['services'] = [targtype]
@@ -507,10 +537,10 @@ def check_fish(urldata, port=443, verifycallback=None):
return data
return None
def _parse_ssdp(peer, rsp, peerdata):
async def _parse_ssdp(peer, rsp, peerdata):
nid = peer[0]
mac = None
mac = neighutil.get_hwaddr(peer[0])
mac = await neighutil.get_hwaddr(peer[0])
if mac:
nid = mac
headlines = rsp.split(b'\r\n')
@@ -21,7 +21,7 @@
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages as msg
import eventlet
import confluent.tasks as tasks
import io
import os
import pwd
@@ -31,12 +31,12 @@ import traceback
updatesbytarget = {}
uploadsbytarget = {}
downloadsbytarget = {}
updatepool = eventlet.greenpool.GreenPool(256)
_tracelog = None
sharedfiles = {}
updatepool = tasks.TaskPool(max_concurrent=256)
def execupdate(handler, filename, updateobj, type, owner, node, datfile):
async def execupdate(handler, filename, updateobj, type, owner, node, datfile):
global _tracelog
try:
if type != 'ffdc' and not datfile:
@@ -66,10 +66,10 @@ def execupdate(handler, filename, updateobj, type, owner, node, datfile):
return
try:
if type == 'firmware':
completion = handler(filename, progress=updateobj.handle_progress,
completion = await handler(filename, progress=updateobj.handle_progress,
data=datfile, bank=updateobj.bank)
else:
completion = handler(filename, progress=updateobj.handle_progress,
completion = await handler(filename, progress=updateobj.handle_progress,
data=datfile)
if type == 'ffdc' and completion:
filename = completion
@@ -122,7 +122,7 @@ class Updater(object):
else:
datfile = None
self.datfile = datfile
self.updateproc = updatepool.spawn(execupdate, handler, filename,
self.updateproc = updatepool.schedule(execupdate, handler, filename,
self, type, owner, node, datfile)
if type == 'firmware':
myparty = updatesbytarget
@@ -145,7 +145,7 @@ class Updater(object):
self.detail = progress.get('detail', '')
def cancel(self):
self.updateproc.kill()
self.updateproc.cancel()
if self.datfile:
self.datfile.close()
+45 -27
View File
@@ -17,9 +17,9 @@
#This handles port forwarding for web interfaces on management devices
#It will also hijack port 3900 and do best effort..
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
import asyncio
import socket
import confluent.tasks as tasks
forwardersbyclient = {}
relaysbysession = {}
sessionsbyip = {}
@@ -28,24 +28,37 @@ sockhandler = {}
vidtargetbypeer = {}
vidforwarder = None
def handle_connection(incoming, outgoing):
while True:
r, _, _ = select.select((incoming, outgoing), (), (), 60)
for mysock in r:
data = mysock.recv(32768)
if not data:
incoming.close()
outgoing.close()
return
if mysock == incoming:
outgoing.sendall(data)
elif mysock == outgoing:
incoming.sendall(data)
async def handle_connection(incoming, outgoing):
async def _relay(reader, writer):
try:
while True:
data = await reader.read(32768)
if not data:
return
writer.write(data)
await writer.drain()
except (ConnectionError, OSError):
return
inrdr, inwriter = await asyncio.open_connection(sock=incoming)
outrdr, outwriter = await asyncio.open_connection(sock=outgoing)
try:
done, pending = await asyncio.wait(
[asyncio.ensure_future(_relay(inrdr, outwriter)),
asyncio.ensure_future(_relay(outrdr, inwriter))],
return_when=asyncio.FIRST_COMPLETED)
for task in pending:
task.cancel()
finally:
inwriter.close()
outwriter.close()
def forward_port(sock, target, clientip, sessionid):
async def forward_port(sock, target, clientip, sessionid):
loop = asyncio.get_event_loop()
sock.setblocking(False)
while True:
conn, cli = sock.accept()
conn, cli = await loop.sock_accept(sock)
if cli[0] != clientip:
conn.close()
continue
@@ -57,14 +70,19 @@ def forward_port(sock, target, clientip, sessionid):
continue
if sessionid not in relaysbysession:
relaysbysession[sessionid] = {}
relaysbysession[sessionid][eventlet.spawn(
handle_connection, conn, client)] = conn
relaysbysession[sessionid][tasks.spawn(
handle_connection(conn, client))] = conn
def forward_video():
sock = eventlet.listen(('::', 3900, 0, 0), family=socket.AF_INET6)
async def forward_video():
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('::', 3900, 0, 0))
sock.listen(50)
loop = asyncio.get_event_loop()
sock.setblocking(False)
while True:
conn, cli = sock.accept()
conn, cli = await loop.sock_accept(sock)
if cli[0] not in vidtargetbypeer or not sessionsbyip.get(cli[0], None):
conn.close()
continue
@@ -76,7 +94,7 @@ def forward_video():
conn.close()
vidclient.close()
continue
eventlet.spawn_n(handle_connection, conn, vidclient)
tasks.spawn(handle_connection(conn, vidclient))
def close_session(sessionid):
@@ -124,10 +142,10 @@ def get_port(addr, clientip, sessionid):
newport += 1
continue
forwardersbyclient[sessionid][addr] = newsock
sockhandler[newsock] = eventlet.spawn(forward_port, newsock, addr,
clientip, sessionid)
sockhandler[newsock] = tasks.spawn(forward_port(newsock, addr,
clientip, sessionid))
if not vidforwarder:
vidforwarder = eventlet.spawn(forward_video)
vidforwarder = tasks.spawn(forward_video())
vidtargetbypeer[clientip] = addr
return forwardersbyclient[sessionid][addr].getsockname()[1]
File diff suppressed because it is too large Load Diff
+9 -3
View File
@@ -63,11 +63,13 @@
# (a future extended version might include suport for Forward Secure Sealing
# or other fields)
import asyncio
import collections
import confluent.config.configmanager
import confluent.config.conf as conf
import confluent.exceptions as exc
import eventlet
import confluent.tasks as tasks
import inspect
import glob
import json
import os
@@ -76,6 +78,7 @@ import stat
import struct
import time
import traceback
import random
try:
unicode
except NameError:
@@ -113,6 +116,7 @@ except ImportError:
MIDNIGHT = 24 * 60 * 60
_loggers = {}
class Events(object):
(
undefined, clearscreen, clientconnect, clientdisconnect,
@@ -632,7 +636,7 @@ class Logger(object):
self.logentries.appendleft([DataTypes.event, tstamp, roll_data,
Events.logrollover, None])
if self.closer is None:
self.closer = eventlet.spawn_after(15, self.closelog)
self.closer = tasks.spawn_task_after(15, self.closelog)
self.writer = None
def read_recent_text(self, size):
@@ -781,7 +785,7 @@ class Logger(object):
[ltype, timestamp, logdata, event, eventdata])
if self.buffered:
if self.writer is None:
self.writer = eventlet.spawn_after(2, self.writedata)
self.writer = tasks.spawn_task_after(2, self.writedata)
else:
self.writedata()
@@ -807,3 +811,5 @@ def logtrace():
tracelog = Logger('trace', buffered=False)
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
tasks.logtrace = logtrace
+4 -4
View File
@@ -25,13 +25,13 @@
# service should have a null tenant and a tenant entry that correlates)
__author__ = 'jjohnson2'
import asyncio
import confluent.config.configmanager as configmanager
import itertools
from eventlet.support import greendns
manager_to_nodemap = {}
def node_by_manager(manager):
async def node_by_manager(manager):
"""Lookup a node by manager
Search for a node according to a given network address.
@@ -46,7 +46,7 @@ def node_by_manager(manager):
"""
manageraddresses = []
for tmpaddr in greendns.getaddrinfo(manager, None):
for tmpaddr in await asyncio.get_event_loop().getaddrinfo(manager, None):
manageraddresses.append(tmpaddr[4][0])
cfm = configmanager.ConfigManager(None)
if manager in manager_to_nodemap:
@@ -68,7 +68,7 @@ def node_by_manager(manager):
if currhm in manageraddresses:
manager_to_nodemap[manager] = node
return node
for curraddr in greendns.getaddrinfo(currhm, None):
for curraddr in await asyncio.get_event_loop().getaddrinfo(currhm, None):
curraddr = curraddr[4][0]
if curraddr in manageraddresses:
manager_to_nodemap[manager] = node
+69 -36
View File
@@ -25,10 +25,14 @@
# Things like heartbeating and discovery
# It also will optionally snoop SLP DA requests
#import logging
#logging.basicConfig(filename='/tmp/asyn.log', level=logging.DEBUG)
import atexit
import confluent.auth as auth
import confluent.config.conf as conf
import confluent.config.configmanager as configmanager
import confluent.debugger as debugger
try:
import anydbm as dbm
except ModuleNotFoundError:
@@ -39,6 +43,7 @@ import confluent.httpapi as httpapi
import confluent.log as log
import confluent.collective.manager as collective
import confluent.discovery.protocols.pxe as pxe
import linecache
try:
import confluent.sockapi as sockapi
except ImportError:
@@ -46,21 +51,14 @@ except ImportError:
#only for now
pass
import confluent.discovery.core as disco
import eventlet
dbgif = False
try:
import eventlet.backdoor as backdoor
dbgif = True
except Exception:
pass
havefcntl = True
try:
import fcntl
except ImportError:
havefcntl = False
#import multiprocessing
import asyncio
import gc
from greenlet import greenlet
import sys
import os
import glob
@@ -73,6 +71,36 @@ import tempfile
import uuid
def format_stack(task):
task.print_stack()
extracted_list = []
checked = set()
for f in task.get_stack():
lineno = f.f_lineno
co = f.f_code
filename = co.co_filename
name = co.co_name
if filename not in checked:
checked.add(filename)
linecache.checkcache(filename)
line = linecache.getline(filename, lineno, f.f_globals)
extracted_list.append((filename, lineno, name, line))
exc = task._exception
if not extracted_list:
yield f'No stack for {task!r}'
elif exc is not None:
yield f'Traceback for {task!r} (most recent call last):'
else:
yield f'Stack for {task!r} (most recent call last):'
for x in traceback.format_list(extracted_list):
yield x
if exc is not None:
for line in traceback.format_exception_only(exc.__class__, exc):
yield line
def _daemonize():
if not 'fork' in os.__dict__:
return
@@ -175,13 +203,9 @@ def dumptrace(signalname, frame):
ht = open('/var/log/confluent/hangtraces', 'a')
ht.write('Dumping active trace on ' + time.strftime('%X %x\n'))
ht.write(''.join(traceback.format_stack(frame)))
for o in gc.get_objects():
if not isinstance(o, greenlet):
continue
if not o:
continue
ht.write('Thread trace: ({0})\n'.format(id(o)))
ht.write(''.join(traceback.format_stack(o.gr_frame)))
for atask in asyncio.all_tasks():
ht.write('Async trace: ({0})\n'.format(id(atask)))
ht.write(''.join([x for x in format_stack(atask)]))
ht.close()
def doexit():
@@ -253,6 +277,9 @@ def migrate_db():
def run(args):
asyncio.run(asyncrun(args))
async def asyncrun(args):
setlimits()
try:
configmanager.ConfigManager(None)
@@ -284,11 +311,16 @@ def run(args):
print(repr(e))
sys.exit(1)
if '-f' not in args:
_daemonize()
sys.stderr.write("-f is now required")
# the fork wreaks havoc with asyncio thread executor
# If someone comes along with a non-systemd demand, will just have to have a daemonize wrapper
sys.exit(1)
#_daemonize()
if '-o' not in args:
_redirectoutput()
if havefcntl:
_updatepidfile()
asyncio.get_event_loop().set_debug(True)
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
atexit.register(doexit)
@@ -298,27 +330,15 @@ def run(args):
configmanager.set_global('confluent_uuid', confluentuuid)
if not configmanager._masterkey:
configmanager.init_masterkey()
if dbgif:
oumask = os.umask(0o077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass # We are not expecting the file to exist
try:
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
except AttributeError:
pass # Windows...
os.umask(oumask)
debugger.start_dbgif()
auth.check_for_yaml()
collective.startup()
consoleserver.initialize()
await consoleserver.initialize()
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
try:
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port)
sockservice.start()
asyncio.get_event_loop().create_task(sockservice.start())
except NameError:
pass
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
@@ -330,12 +350,25 @@ def run(args):
configmanager.check_quorum()
break
except Exception:
eventlet.sleep(0.5)
eventlet.spawn_n(disco.start_detection)
eventlet.sleep(1)
consoleserver.start_console_sessions()
await asyncio.sleep(0.5)
disco.start_detection()
await asyncio.sleep(1)
await consoleserver.start_console_sessions()
notifysock = os.environ.get('NOTIFY_SOCKET', None)
if notifysock:
if notifysock.startswith('@'):
notifysock = '\0' + notifysock[1:]
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.connect(notifysock)
sock.send(b'READY=1')
watchdogsecs = int(os.environ.get('WATCHDOG_USEC', 0)) / 1000000
if not watchdogsecs:
watchdogsecs = 200
watchdogsecs = watchdogsecs / 2
while 1:
eventlet.sleep(100)
await asyncio.sleep(watchdogsecs)
if notifysock:
sock.send(b'WATCHDOG=1')
def _get_connector_config(session):
host = conf.get_option(session, 'bindhost')
+31
View File
@@ -519,6 +519,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputAlertDestination(path, nodes, inputdata, multinode)
elif len(path) == 3 and path[:3] == ['configuration', 'management_controller', 'certificate_authorities'] and operation not in ('retrieve', 'delete'):
return InputCertificateAuthority(path, nodes, inputdata)
elif len(path) == 4 and path[:4] == ['configuration', 'management_controller', 'certificate', 'sign'] and operation not in ('retrieve', 'delete'):
return InputSigningParameters(path, inputdata, nodes, configmanager)
elif path == ['identify'] and operation != 'retrieve':
return InputIdentifyMessage(path, nodes, inputdata)
elif path == ['events', 'hardware', 'decode']:
@@ -578,6 +580,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputLicense(path, nodes, inputdata, configmanager)
elif path == ['deployment', 'lock'] and inputdata:
return InputDeploymentLock(path, nodes, inputdata)
elif path == ['deployment', 'remote_config', 'run'] and inputdata:
return InputRemoteConfig(path, nodes, inputdata)
elif path == ['deployment', 'ident_image']:
return InputIdentImage(path, nodes, inputdata)
elif path == ['console', 'ikvm']:
@@ -603,6 +607,11 @@ class InputFirmwareUpdate(ConfluentMessage):
@property
def filename(self):
# TODO: get the currennt_user and cross reference if that user is allowed to
# read... however, not sure wwhat to do if user is pure confluent user
# though the staging may get an explicit pass, which should cover the web case...
# media and firmware are ways to currently push things out, but if we allow profile export
# what then?
if self._complexname:
raise Exception('User requested substitutions, but code is '
'written against old api, code must be fixed or '
@@ -721,6 +730,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 +968,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 +1001,10 @@ class InputDeploymentLock(ConfluentInputMessage):
keyname = 'lock'
valid_values = ['autolock', 'unlocked', 'locked']
class InputRemoteConfig(ConfluentInputMessage):
keyname = 'category'
valid_values = ['post.d', 'firstboot.d', 'onboot.d']
class DeploymentLock(ConfluentChoiceMessage):
valid_values = set([
'autolock',
@@ -1703,6 +1733,7 @@ class Disk(ConfluentMessage):
state_aliases = {
'unconfigured bad': 'fault',
'unconfigured good': 'unconfigured',
'unconfiguredgood': 'unconfigured',
'global hot spare': 'hotspare',
'dedicated hot spare': 'hotspare',
}
+20 -22
View File
@@ -1,29 +1,28 @@
import eventlet
import asyncio
import confluent.messages as msg
import confluent.exceptions as exc
import struct
import eventlet.green.socket as socket
import eventlet.green.subprocess as subprocess
import socket
import os
mountsbyuser = {}
_browserfsd = None
def assure_browserfs():
async def assure_browserfs():
global _browserfsd
if _browserfsd is None:
os.makedirs('/var/run/confluent/browserfs/mount', exist_ok=True)
_browserfsd = subprocess.Popen(
['/opt/confluent/bin/browserfs',
_browserfsd = await asyncio.subprocess.create_subprocess_exec(
'/opt/confluent/bin/browserfs',
'-c', '/var/run/confluent/browserfs/control',
'-s', '127.0.0.1:4006',
# browserfs supports unix domain websocket, however apache reverse proxy is dicey that way in some versions
'-w', '/var/run/confluent/browserfs/mount'])
'-w', '/var/run/confluent/browserfs/mount')
while not os.path.exists('/var/run/confluent/browserfs/control'):
eventlet.sleep(0.5)
await asyncio.sleep(0.5)
def handle_request(configmanager, inputdata, pathcomponents, operation):
async def handle_request(configmanager, inputdata, pathcomponents, operation):
curruser = configmanager.current_user
if len(pathcomponents) == 0:
mounts = mountsbyuser.get(curruser, [])
@@ -39,7 +38,7 @@ def handle_request(configmanager, inputdata, pathcomponents, operation):
curridx = 1
while curridx in usedidx:
curridx += 1
currmount = requestmount(curruser, inputdata['name'])
currmount = await requestmount(curruser, inputdata['name'])
currmount['index'] = curridx
if curruser not in mountsbyuser:
mountsbyuser[curruser] = []
@@ -50,26 +49,25 @@ def handle_request(configmanager, inputdata, pathcomponents, operation):
'authtoken': currmount['authtoken']
})
def requestmount(subdir, filename):
assure_browserfs()
async def requestmount(subdir, filename):
await assure_browserfs()
cloop = asyncio.get_event_loop()
a = socket.socket(socket.AF_UNIX)
a.connect('/var/run/confluent/browserfs/control')
a.settimeout(0)
await cloop.sock_connect(a, '/var/run/confluent/browserfs/control')
subname = subdir.encode()
a.send(struct.pack('!II', 1, len(subname)))
a.send(subname)
fname = filename.encode()
a.send(struct.pack('!I', len(fname)))
a.send(fname)
rsp = a.recv(4)
await cloop.sock_sendall(a, struct.pack('!II', 1, len(subname)) + subname + struct.pack('!I', len(fname)) + fname)
rsp = await cloop.sock_recv(a, 4)
retcode = struct.unpack('!I', rsp)[0]
if retcode != 0:
raise Exception("Bad return code")
rsp = a.recv(4)
rsp = await cloop.sock_recv(a, 4)
nlen = struct.unpack('!I', rsp)[0]
idstr = a.recv(nlen).decode('utf8')
rsp = a.recv(4)
idstr = (await cloop.sock_recv(a, nlen)).decode('utf8')
rsp = await cloop.sock_recv(a, 4)
nlen = struct.unpack('!I', rsp)[0]
authtok = a.recv(nlen).decode('utf8')
authtok = (await cloop.sock_recv(a, nlen)).decode('utf8')
thismount = {
'id': idstr,
'path': '{}/{}/{}'.format(idstr, subdir, filename),
+14 -11
View File
@@ -16,11 +16,11 @@
# A consolidated manage of neighbor table information management.
import asyncio
import confluent.netutil as netutil
import confluent.util as util
import os
import eventlet.semaphore as semaphore
import eventlet.green.socket as socket
import socket
import struct
@@ -33,25 +33,28 @@ neightime = 0
import re
neighlock = semaphore.Semaphore()
neighlock = asyncio.Lock()
def _update_neigh():
async def _update_neigh():
global neightable
global neightime
neightime = os.times()[4]
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((0, 0))
s.settimeout(0)
# RTM_GETNEIGH
# nlmsghdr struct: u32 len, u16 type, u16 flags, u32 seq, u32 pid
nlhdr = b'\x1c\x00\x00\x00\x1e\x00\x01\x03\x00\x00\x00\x00\x00\x00\x00\x00'
# ndmsg struct u8 family u8 pad, u16 pad, s32 ifidx, u16 state, u8 flags, u8 type
ndmsg= b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
s.sendall(nlhdr + ndmsg)
cloop = asyncio.get_event_loop()
await cloop.sock_sendall(s, nlhdr + ndmsg)
#s.sendall(nlhdr + ndmsg)
neightable = {}
inprogress = True
try:
while inprogress:
pdata = s.recv(65536)
pdata = await cloop.sock_recv(s, 65536)
v = memoryview(pdata)
while len(v):
length, typ = struct.unpack('IH', v[:6])
@@ -82,7 +85,7 @@ def _update_neigh():
s.close()
def get_hwaddr(ipaddr):
async def get_hwaddr(ipaddr):
if '%' in ipaddr:
ipaddr, _ = ipaddr.split('%', 1)
hwaddr = None
@@ -92,16 +95,16 @@ def get_hwaddr(ipaddr):
ipaddr = socket.inet_pton(socket.AF_INET6, ipaddr)
elif '.' in ipaddr:
ipaddr = socket.inet_pton(socket.AF_INET, ipaddr)
with neighlock:
async with neighlock:
updated = False
if os.times()[4] > (neightime + 30):
_update_neigh()
await _update_neigh()
updated = True
hwaddr = neightable.get(ipaddr, None)
if not hwaddr and not netutil.ipn_is_local(ipaddr):
if not hwaddr and not await netutil.ipn_is_local(ipaddr):
hwaddr = False
if hwaddr == None and not updated:
_update_neigh()
await _update_neigh()
hwaddr = neightable.get(ipaddr, None)
if hwaddr:
hwaddr = ':'.join(['{:02x}'.format(x) for x in bytearray(hwaddr)])
+60 -62
View File
@@ -16,6 +16,7 @@
# this will implement noderange grammar
import asyncio
import confluent.exceptions as exc
import codecs
try:
@@ -24,13 +25,10 @@ except ImportError:
psutil = None
import netifaces
import struct
import eventlet.green.socket as socket
import eventlet.support.greendns
import os
getaddrinfo = eventlet.support.greendns.getaddrinfo
import socket
import confluent.tasks as tasks
eventlet.support.greendns.resolver.clear()
eventlet.support.greendns.resolver._resolver.lifetime = 1
def msg_align(len):
return (len + 3) & ~3
@@ -74,18 +72,18 @@ def ipn_on_same_subnet(fam, first, second, prefix):
second = struct.unpack('!I', second)[0]
return (first & mask == second & mask)
def ip_on_same_subnet(first, second, prefix):
async def ip_on_same_subnet(first, second, prefix):
if first.startswith('::ffff:') and '.' in first:
first = first.replace('::ffff:', '')
if second.startswith('::ffff:') and '.' in second:
second = second.replace('::ffff:', '')
addrinf = socket.getaddrinfo(first, None, 0, socket.SOCK_STREAM)[0]
addrinf = (await asyncio.get_running_loop().getaddrinfo(first, 0, type=socket.SOCK_STREAM))[0]
fam = addrinf[0]
if '%' in addrinf[-1][0]:
return False
ip = socket.inet_pton(fam, addrinf[-1][0])
ip = int(codecs.encode(bytes(ip), 'hex'), 16)
addrinf = socket.getaddrinfo(second, None, 0, socket.SOCK_STREAM)[0]
addrinf = (await asyncio.get_running_loop().getaddrinfo(second, 0, type=socket.SOCK_STREAM))[0]
if fam != addrinf[0]:
return False
txtaddr = addrinf[-1][0].split('%')[0]
@@ -101,10 +99,10 @@ def ip_on_same_subnet(first, second, prefix):
return ip & mask == oip & mask
def ipn_is_local(ipn):
async def ipn_is_local(ipn):
if len(ipn) > 5 and ipn.startswith(b'\xfe\x80'):
return True
for addr in get_my_addresses():
for addr in await get_my_addresses():
if len(addr[1]) != len(ipn):
continue
if ipn_on_same_subnet(addr[0], ipn, addr[1], addr[2]):
@@ -112,25 +110,25 @@ def ipn_is_local(ipn):
return False
def address_is_local(address):
async def address_is_local(address):
if psutil:
ifas = psutil.net_if_addrs()
for iface in ifas:
for addr in ifas[iface]:
if addr.family in (socket.AF_INET, socket.AF_INET6):
cidr = mask_to_cidr(addr.netmask)
if ip_on_same_subnet(addr.address, address, cidr):
if await ip_on_same_subnet(addr.address, address, cidr):
return True
else:
for iface in netifaces.interfaces():
for i4 in netifaces.ifaddresses(iface).get(2, []):
cidr = mask_to_cidr(i4['netmask'])
if ip_on_same_subnet(i4['addr'], address, cidr):
if await ip_on_same_subnet(i4['addr'], address, cidr):
return True
for i6 in netifaces.ifaddresses(iface).get(10, []):
cidr = int(i6['netmask'].split('/')[1])
laddr = i6['addr'].split('%')[0]
if ip_on_same_subnet(laddr, address, cidr):
if await ip_on_same_subnet(laddr, address, cidr):
return True
return False
@@ -146,7 +144,7 @@ def _rebuildidxmap():
pass
def myiptonets(svrip):
async def myiptonets(svrip):
fam = socket.AF_INET
if ':' in svrip:
fam = socket.AF_INET6
@@ -159,7 +157,7 @@ def myiptonets(svrip):
continue
addr = addr.address
addr = addr.split('%')[0]
if addresses_match(addr, svrip):
if await addresses_match(addr, svrip):
relevantnic = iface
break
else:
@@ -170,7 +168,7 @@ def myiptonets(svrip):
for addr in netifaces.ifaddresses(iface).get(fam, []):
addr = addr.get('addr', '')
addr = addr.split('%')[0]
if addresses_match(addr, svrip):
if await addresses_match(addr, svrip):
relevantnic = iface
break
else:
@@ -220,13 +218,12 @@ class NetManager(object):
self.consumednames4 = set([])
self.consumednames6 = set([])
@property
def allmyaddrs(self):
async def allmyaddrs(self):
if not self._allmyaddrs:
self._allmyaddrs = get_my_addresses()
self._allmyaddrs = await get_my_addresses()
return self._allmyaddrs
def process_attribs(self, netname, attribs):
async def process_attribs(self, netname, attribs):
self.myattribs[netname] = {}
ipv4addr = None
ipv6addr = None
@@ -255,7 +252,7 @@ class NetManager(object):
if ipv4addr:
try:
luaddr = ipv4addr.split('/', 1)[0]
for ai in socket.getaddrinfo(luaddr, 0, socket.AF_INET, socket.SOCK_STREAM):
for ai in await asyncio.get_running_loop().getaddrinfo(luaddr, 0, family=socket.AF_INET, type=socket.SOCK_STREAM):
ipv4addr.replace(luaddr, ai[-1][0])
except socket.gaierror:
pass
@@ -263,7 +260,7 @@ class NetManager(object):
currname = attribs.get('hostname', self.node).split()[0]
if currname and currname not in self.consumednames4:
try:
for ai in socket.getaddrinfo(currname, 0, socket.AF_INET, socket.SOCK_STREAM):
for ai in await asyncio.get_running_loop().getaddrinfo(currname, 0, family=socket.AF_INET, type=socket.SOCK_STREAM):
ipv4addr = ai[-1][0]
self.consumednames4.add(currname)
except socket.gaierror:
@@ -280,7 +277,7 @@ class NetManager(object):
ipv6addr = attribs.get('ipv6_address', None)
if ipv6addr:
try:
for ai in socket.getaddrinfo(ipv6addr, 0, socket.AF_INET6, socket.SOCK_STREAM):
for ai in await asyncio.get_running_loop().getaddrinfo(ipv6addr, 0, family=socket.AF_INET6, type=socket.SOCK_STREAM):
ipv6addr = ai[-1][0]
except socket.gaierror:
pass
@@ -288,7 +285,7 @@ class NetManager(object):
currname = attribs.get('hostname', self.node).split()[0]
if currname and currname not in self.consumednames6:
try:
for ai in socket.getaddrinfo(currname, 0, socket.AF_INET6, socket.SOCK_STREAM):
for ai in await asyncio.get_running_loop().getaddrinfo(currname, 0, family=socket.AF_INET6, type=socket.SOCK_STREAM):
ipv6addr = ai[-1][0]
self.consumednames6.add(currname)
except socket.gaierror:
@@ -327,7 +324,7 @@ class NetManager(object):
if '/' not in myattribs.get('ipv6_address', '/'):
ipn = socket.inet_pton(socket.AF_INET6, myattribs['ipv6_address'])
plen = 64
for addr in self.allmyaddrs:
for addr in await self.allmyaddrs():
if addr[0] != socket.AF_INET6:
continue
if ipn_on_same_subnet(addr[0], ipn, addr[1], addr[2]):
@@ -336,7 +333,7 @@ class NetManager(object):
if '/' not in myattribs.get('ipv4_address', '/'):
ipn = socket.inet_pton(socket.AF_INET, myattribs['ipv4_address'])
plen = 16
for addr in self.allmyaddrs:
for addr in await self.allmyaddrs():
if addr[0] != socket.AF_INET:
continue
if ipn_on_same_subnet(addr[0], ipn, addr[1], addr[2]):
@@ -346,8 +343,8 @@ class NetManager(object):
myattribs['current_nic'] = False
def get_flat_net_config(configmanager, node):
fnc = get_full_net_config(configmanager, node)
async def get_flat_net_config(configmanager, node):
fnc = await get_full_net_config(configmanager, node)
dft = fnc.get('default', {})
if dft:
ret = [dft]
@@ -364,7 +361,7 @@ def add_netmask(ncfg):
plen = ncfg['ipv4_address'].split('/', 1)[1]
ncfg['ipv4_netmask'] = cidr_to_mask(int(plen))
def get_full_net_config(configmanager, node, serverip=None):
async def get_full_net_config(configmanager, node, serverip=None):
cfd = configmanager.get_node_attributes(node, ['net.*'])
cfd = cfd.get(node, {})
bmc = configmanager.get_node_attributes(
@@ -374,11 +371,11 @@ def get_full_net_config(configmanager, node, serverip=None):
bmc6 = None
if bmc:
try:
bmc4 = socket.getaddrinfo(bmc, 0, socket.AF_INET, socket.SOCK_DGRAM)[0][-1][0]
bmc4 = (await asyncio.get_running_loop().getaddrinfo(bmc, 0, family=socket.AF_INET, type=socket.SOCK_DGRAM))[0][-1][0]
except Exception:
pass
try:
bmc6 = socket.getaddrinfo(bmc, 0, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1][0]
bmc6 = (await asyncio.get_running_loop().getaddrinfo(bmc, 0, family=socket.AF_INET6, type=socket.SOCK_DGRAM))[0][-1][0]
except Exception:
pass
attribs = {}
@@ -400,16 +397,16 @@ def get_full_net_config(configmanager, node, serverip=None):
attribs[iface][attrib] = val
myaddrs = []
if serverip:
myaddrs = get_addresses_by_serverip(serverip)
myaddrs = await get_addresses_by_serverip(serverip)
nm = NetManager(myaddrs, node, configmanager)
defaultnic = {}
ppool = eventlet.greenpool.GreenPool(64)
ppool = tasks.TaskPool()
if None in attribs:
ppool.spawn(nm.process_attribs, None, attribs[None])
ppool.schedule(nm.process_attribs, None, attribs[None])
del attribs[None]
for netname in sorted(attribs):
ppool.spawn(nm.process_attribs, netname, attribs[netname])
ppool.waitall()
ppool.schedule(nm.process_attribs, netname, attribs[netname])
await ppool.waitall()
for iface in list(nm.myattribs):
if bmc4 and nm.myattribs[iface].get('ipv4_address', None) == bmc4:
del nm.myattribs[iface]
@@ -422,7 +419,7 @@ def get_full_net_config(configmanager, node, serverip=None):
add_netmask(retattrs['default'])
del nm.myattribs[None]
else:
nnc = get_nic_config(configmanager, node, serverip=serverip)
nnc = await get_nic_config(configmanager, node, serverip=serverip)
if nnc.get('ipv4_address', None):
defaultnic['ipv4_address'] = '{}/{}'.format(nnc['ipv4_address'], nnc['prefix'])
if nnc.get('ipv4_gateway', None):
@@ -477,7 +474,7 @@ def noneify(cfgdata):
# that mac address
# the ip as reported by recvmsg to match the subnet of that net.* interface
# if switch and port available, that should match.
def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
async def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
serverip=None, relayipn=b'\x00\x00\x00\x00',
clientip=None, onlyfamily=None):
"""Fetch network configuration parameters for a nic
@@ -533,12 +530,12 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
if bmc:
try:
if onlyfamily in (0, socket.AF_INET):
bmc4 = socket.getaddrinfo(bmc, 0, socket.AF_INET, socket.SOCK_DGRAM)[0][-1][0]
bmc4 = (await asyncio.get_running_loop().getaddrinfo(bmc, 0, family=socket.AF_INET, type=socket.SOCK_DGRAM))[0][-1][0]
except Exception:
pass
try:
if onlyfamily in (0, socket.AF_INET6):
bmc6 = socket.getaddrinfo(bmc, 0, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1][0]
bmc6 = (await asyncio.get_running_loop().getaddrinfo(bmc, 0, family=socket.AF_INET6, type=socket.SOCK_DGRAM))[0][-1][0]
except Exception:
pass
cfgbyname = {}
@@ -564,7 +561,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
myaddrs = []
if ifidx is not None:
dhcprequested = False
myaddrs = get_my_addresses(ifidx, family=onlyfamily)
myaddrs = await get_my_addresses(ifidx, family=onlyfamily)
v4broken = True
v6broken = True
for addr in myaddrs:
@@ -579,7 +576,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
isremote = False
if serverip is not None:
dhcprequested = False
myaddrs = get_addresses_by_serverip(serverip)
myaddrs = await get_addresses_by_serverip(serverip)
if serverfam == socket.AF_INET6 and ipn_on_same_subnet(serverfam, serveripn, llaipn, 64):
isremote = False
elif clientfam:
@@ -597,13 +594,13 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
ip6bynodename = None
try:
if onlyfamily in (socket.AF_INET, 0):
for addr in socket.getaddrinfo(node, 0, socket.AF_INET, socket.SOCK_DGRAM):
for addr in await asyncio.get_running_loop().getaddrinfo(node, 0, family=socket.AF_INET, type=socket.SOCK_DGRAM):
ipbynodename = addr[-1][0]
except socket.gaierror:
pass
try:
if onlyfamily in (socket.AF_INET6, 0):
for addr in socket.getaddrinfo(node, 0, socket.AF_INET6, socket.SOCK_DGRAM):
for addr in await asyncio.get_running_loop().getaddrinfo(node, 0, family=socket.AF_INET6, type=socket.SOCK_DGRAM):
ip6bynodename = addr[-1][0]
except socket.gaierror:
pass
@@ -650,7 +647,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
if bmc6 and candip == bmc6:
continue
try:
for inf in socket.getaddrinfo(candip, 0, fam, socket.SOCK_STREAM):
for inf in await asyncio.get_running_loop().getaddrinfo(candip, 0, family=fam, type=socket.SOCK_STREAM):
candipn = socket.inet_pton(fam, inf[-1][0])
if ((isremote and ipn_on_same_subnet(fam, clientipn, candipn, int(candprefix)))
or ipn_on_same_subnet(fam, bootsvrip, candipn, prefix)):
@@ -669,7 +666,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
except Exception as e:
cfgdata['error_msg'] = "Error trying to evaluate net.*ipv4_address attribute value '{0}' on {1}: {2}".format(candip, node, str(e))
elif candgw:
for inf in socket.getaddrinfo(candgw, 0, fam, socket.SOCK_STREAM):
for inf in await asyncio.get_running_loop().getaddrinfo(candgw, 0, family=fam, type=socket.SOCK_STREAM):
candgwn = socket.inet_pton(fam, inf[-1][0])
if ipn_on_same_subnet(fam, bootsvrip, candgwn, prefix):
candgws.append((fam, candgwn, prefix))
@@ -731,7 +728,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
cfgdata['ipv{}_gateway'.format(nver)] = socket.inet_ntop(fam, candgwn)
return noneify(cfgdata)
if ip is not None:
for prefixinfo in get_prefix_len_for_ip(ip):
async for prefixinfo in get_prefix_len_for_ip(ip):
fam, prefix = prefixinfo
ip = ip.split('/', 1)[0]
if fam == socket.AF_INET:
@@ -747,14 +744,14 @@ def get_nic_config(configmanager, node, ip=None, mac=None, ifidx=None,
if gw is None or not gw:
continue
gwn = socket.inet_pton(fam, gw)
ip = socket.getaddrinfo(ip, 0, proto=socket.IPPROTO_TCP, family=fam)[-1][-1][0]
ip = (await asyncio.get_running_loop().getaddrinfo(ip, 0, proto=socket.IPPROTO_TCP, family=fam))[-1][-1][0]
ipn = socket.inet_pton(fam, ip)
if ipn_on_same_subnet(fam, ipn, gwn, prefix):
cfgdata['ipv{}_gateway'.format(nver)] = gw
break
return noneify(cfgdata)
def get_addresses_by_serverip(serverip):
async def get_addresses_by_serverip(serverip):
if '.' in serverip:
fam = socket.AF_INET
elif ':' in serverip:
@@ -763,15 +760,15 @@ def get_addresses_by_serverip(serverip):
raise ValueError('"{0}" is not a valid ip argument'.format(serverip))
ipbytes = socket.inet_pton(fam, serverip)
if ipbytes[:8] == b'\xfe\x80\x00\x00\x00\x00\x00\x00':
myaddrs = get_my_addresses(matchlla=ipbytes)
myaddrs = await get_my_addresses(matchlla=ipbytes)
else:
myaddrs = [x for x in get_my_addresses() if x[1] == ipbytes]
myaddrs = [x for x in await get_my_addresses() if x[1] == ipbytes]
return myaddrs
nlhdrsz = struct.calcsize('IHHII')
ifaddrsz = struct.calcsize('BBBBI')
def get_my_addresses(idx=0, family=0, matchlla=None):
async def get_my_addresses(idx=0, family=0, matchlla=None):
# 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)
@@ -779,10 +776,11 @@ def get_my_addresses(idx=0, family=0, matchlla=None):
ifaddrmsg = struct.pack('BBBBI', family, 0, 0, 0, idx)
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, socket.NETLINK_ROUTE)
s.bind((0, 0))
s.sendall(nlhdr + ifaddrmsg)
s.setblocking(False)
await asyncio.get_event_loop().sock_sendall(s, nlhdr + ifaddrmsg)
addrs = []
while True:
pdata = s.recv(65536)
pdata = await asyncio.get_event_loop().sock_recv(s, 65536)
v = memoryview(pdata)
if struct.unpack('H', v[4:6])[0] == 3: # netlink done message
break
@@ -798,7 +796,7 @@ def get_my_addresses(idx=0, family=0, matchlla=None):
if rtalen < 4:
break
if rta[4:rtalen].tobytes() == matchlla:
return get_my_addresses(idx=ridx)
return await get_my_addresses(idx=ridx)
rta = rta[msg_align(rtalen):]
elif (ridx == idx or not idx) and scope == 0:
rta = v[nlhdrsz+ifaddrsz:length]
@@ -813,14 +811,14 @@ def get_my_addresses(idx=0, family=0, matchlla=None):
return addrs
def get_prefix_len_for_ip(ip):
async def get_prefix_len_for_ip(ip):
plen = None
if '/' in ip:
ip, plen = ip.split('/', 1)
plen = int(plen)
myaddrs = get_my_addresses()
myaddrs = await get_my_addresses()
found = False
for inf in socket.getaddrinfo(ip, 0, 0, socket.SOCK_DGRAM):
for inf in await asyncio.get_running_loop().getaddrinfo(ip, 0, type=socket.SOCK_DGRAM):
if plen:
yield (inf[0], plen)
return
@@ -833,7 +831,7 @@ def get_prefix_len_for_ip(ip):
if not found:
raise exc.NotImplementedException("Non local addresses not supported")
def addresses_match(addr1, addr2):
async def addresses_match(addr1, addr2):
"""Check two network addresses for similarity
Is it zero padded in one place, not zero padded in another? Is one place by name and another by IP??
@@ -846,12 +844,12 @@ def addresses_match(addr1, addr2):
"""
if '%' in addr1 or '%' in addr2:
return False
for addrinfo in socket.getaddrinfo(addr1, 0, 0, socket.SOCK_STREAM):
for addrinfo in await asyncio.get_running_loop().getaddrinfo(addr1, 0, type=socket.SOCK_STREAM):
rootaddr1 = socket.inet_pton(addrinfo[0], addrinfo[4][0])
if addrinfo[0] == socket.AF_INET6 and rootaddr1[:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
# normalize to standard IPv4
rootaddr1 = rootaddr1[-4:]
for otherinfo in socket.getaddrinfo(addr2, 0, 0, socket.SOCK_STREAM):
for otherinfo in await asyncio.get_running_loop().getaddrinfo(addr2, 0, type=socket.SOCK_STREAM):
otheraddr = socket.inet_pton(otherinfo[0], otherinfo[4][0])
if otherinfo[0] == socket.AF_INET6 and otheraddr[:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
otheraddr = otheraddr[-4:]
+88 -51
View File
@@ -17,7 +17,7 @@
# This provides the implementation of locating MAC addresses on ethernet
# switches. It is, essentially, a port of 'MacMap.pm' to confluent.
# However, there are enhancements.
# For one, each switch interrogation is handled in an eventlet 'thread'
# For one, each switch interrogation is handled in an async coroutine
# For another, MAC addresses are checked in the dictionary on every
# switch return, rather than waiting for all switches to check in
# (which makes it more responsive when there is a missing or bad switch)
@@ -33,19 +33,21 @@
if __name__ == '__main__':
import sys
import confluent.config.configmanager as cfm
import asyncio
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
import confluent.snmputil as snmp
import confluent.networking.netutil as netutil
import confluent.util as util
import eventlet
from eventlet.greenpool import GreenPool
import eventlet.semaphore
import confluent.tasks as tasks
import re
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
# The interesting OIDs are:
# lldpLocChassisId - to cross reference (1.0.8802.1.1.2.1.3.2.0)
# lldpLocPortId - for cross referencing.. (1.0.8802.1.1.2.1.3.7.1.3)
@@ -94,7 +96,7 @@ def lenovoname(idx, desc):
return desc
nameoverrides = [
(re.compile('20301\..*'), lenovoname),
(re.compile(r'20301\..*'), lenovoname),
]
# Lenovo chassis id rule is match only first 5 bytes for a match.....
@@ -176,39 +178,45 @@ def _init_lldp(data, iname, idx, idxtoportid, switch):
'chassisid': _chassisidbyswitch[switch]}
_fastbackends = {}
def detect_backend(switch, verifier):
async def detect_backend(switch, verifier):
backend = _fastbackends.get(switch, None)
if backend:
return backend
wc = webclient.SecureHTTPConnection(
wc = webclient.WebConnection(
switch, 443, verifycallback=verifier, timeout=5)
apicheck, retcode = wc.grab_json_response_with_status('/affluent/')
apicheck, retcode = await wc.grab_json_response_with_status('/affluent/')
if retcode == 401 and apicheck.startswith(b'{}'):
_fastbackends[switch] = 'affluent'
else:
apicheck, retcode = wc.grab_json_response_with_status('/api/')
apicheck, retcode = await wc.grab_json_response_with_status('/api/')
if retcode == 400 and apicheck.startswith(b'{"imdata":['):
_fastbackends[switch] = 'nxapi'
else:
rsp = await wc.grab_response_with_status('/jsonrpc', {'dummy': 'data'}, expect_type='json')
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):
async def _extract_neighbor_data_https(switch, user, password, cfm, lldpdata):
kv = util.TLSCertVerifier(cfm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
backend = detect_backend(switch, kv)
backend = await detect_backend(switch, kv)
if not backend:
raise Exception("No HTTPS backend identified")
wc = webclient.SecureHTTPConnection(
wc = webclient.WebConnection(
switch, 443, verifycallback=kv, timeout=5)
if backend == 'affluent':
return _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc)
return await _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc)
elif backend == 'nxapi':
return _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc)
return await _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc)
elif backend == 'srlinux':
return await _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc)
def _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc):
async def _extract_neighbor_data_nxapi(switch, user, password, cfm, lldpdata, wc):
cli = nxapi.NxApiClient(switch, user, password, cfm)
lldpinfo = cli.get_lldp()
lldpinfo = await cli.get_lldp()
for port in lldpinfo:
portdata = lldpinfo[port]
peerid = '{0}.{1}'.format(
@@ -217,13 +225,32 @@ 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_affluent(switch, user, password, cfm, lldpdata, wc):
async def _extract_neighbor_data_srlinux(switch, user, password, cfm, lldpdata, wc):
cli = srlinux.SRLinuxClient(switch, user, password, cfm)
await cli.login()
lldpinfo = await 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
async 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')
neighdata = await wc.grab_json_response('/affluent/lldp/all')
chassisid = neighdata['chassis']['id']
_chassisidbyswitch[switch] = chassisid,
for record in neighdata['neighbors']:
@@ -250,58 +277,67 @@ def _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata, wc):
_neighdata[switch] = lldpdata
def _extract_neighbor_data_b(args):
async def _extract_neighbor_data_b(args):
"""Build LLDP data about elements connected to switch
args are carried as a tuple, because of eventlet convenience
args are carried as a tuple
"""
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:
return
lldpdata = {'!!vintage': now}
try:
return _extract_neighbor_data_https(switch, user, password, cfm, lldpdata)
return await _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'):
async for sysid in conn.walk('1.3.6.1.2.1.1.2'):
sid = str(sysid[1][6:])
_noaffluent.add(switch)
idxtoifname = {}
idxtoportid = {}
_chassisidbyswitch[switch] = sanitize(list(
conn.walk('1.0.8802.1.1.2.1.3.2'))[0][1])
for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.3'):
async for cid in conn.walk('1.0.8802.1.1.2.1.3.2'):
_chassisidbyswitch[switch] = sanitize(cid[1])
break
#_chassisidbyswitch[switch] = sanitize(list(
# conn.walk('1.0.8802.1.1.2.1.3.2'))[0][1])
async for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.3'):
idx = oidindex[0][-1]
idxtoportid[idx] = sanitize(oidindex[1])
for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.4'):
async for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.4'):
idx = oidindex[0][-1]
idxtoifname[idx] = _lldpdesc_to_ifname(sid, idx, str(oidindex[1]))
for remotedesc in conn.walk('1.0.8802.1.1.2.1.4.1.1.10'):
async for remotedesc in conn.walk('1.0.8802.1.1.2.1.4.1.1.10'):
iname = idxtoifname.get(remotedesc[0][-2],
idxtoportid.get(remotedesc[0][-2], None))
if iname is None:
continue
_init_lldp(lldpdata, iname, remotedesc[0][-2], idxtoportid, switch)
_extract_extended_desc(lldpdata[iname], remotedesc[1], user)
for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.9'):
async for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.9'):
iname = idxtoifname.get(remotename[0][-2],
idxtoportid.get(remotename[0][-2], None))
if iname is None:
continue
_init_lldp(lldpdata, iname, remotename[0][-2], idxtoportid, switch)
lldpdata[iname]['peername'] = str(remotename[1])
for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.7'):
async for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.7'):
iname = idxtoifname.get(remotename[0][-2],
idxtoportid.get(remotename[0][-2], None))
if iname is None:
continue
_init_lldp(lldpdata, iname, remotename[0][-2], idxtoportid, switch)
lldpdata[iname]['peerportid'] = sanitize(remotename[1])
for remoteid in conn.walk('1.0.8802.1.1.2.1.4.1.1.5'):
async for remoteid in conn.walk('1.0.8802.1.1.2.1.4.1.1.5'):
iname = idxtoifname.get(remoteid[0][-2],
idxtoportid.get(remoteid[0][-2], None))
if iname is None:
@@ -321,19 +357,20 @@ def _extract_neighbor_data_b(args):
_neighdata[switch] = lldpdata
def update_switch_data(switch, configmanager, force=False, retexc=False):
async def update_switch_data(switch, configmanager, force=False, retexc=False):
switchcreds = netutil.get_switchcreds(configmanager, (switch,))[0]
ndr = _extract_neighbor_data(switchcreds + (force, retexc))
ndr = await _extract_neighbor_data(switchcreds + (force, retexc))
if retexc and isinstance(ndr, Exception):
raise ndr
return _neighdata.get(switch, {})
def update_neighbors(configmanager, force=False, retexc=False):
return _update_neighbors_backend(configmanager, force, retexc)
async def update_neighbors(configmanager, force=False, retexc=False):
async for ans in _update_neighbors_backend(configmanager, force, retexc):
yield ans
def _update_neighbors_backend(configmanager, force, retexc):
async def _update_neighbors_backend(configmanager, force, retexc):
global _neighdata
global _neighbypeerid
vintage = _neighdata.get('!!vintage', 0)
@@ -345,27 +382,26 @@ def _update_neighbors_backend(configmanager, force, retexc):
switches = netutil.list_switches(configmanager)
switchcreds = netutil.get_switchcreds(configmanager, switches)
switchcreds = [ x + (force, retexc) for x in switchcreds]
pool = GreenPool(64)
for ans in pool.imap(_extract_neighbor_data, switchcreds):
async for ans in tasks.task_imap(_extract_neighbor_data, switchcreds, max_concurrent=64):
yield ans
def _extract_neighbor_data(args):
async def _extract_neighbor_data(args):
# single switch neighbor data update
switch = args[0]
if switch not in _updatelocks:
_updatelocks[switch] = eventlet.semaphore.Semaphore()
_updatelocks[switch] = asyncio.Semaphore()
if _updatelocks[switch].locked():
while _updatelocks[switch].locked():
eventlet.sleep(1)
await asyncio.sleep(1)
return
try:
with _updatelocks[switch]:
return _extract_neighbor_data_b(args)
async with _updatelocks[switch]:
return await _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:
@@ -376,6 +412,7 @@ if __name__ == '__main__':
# (should do three argument form for snmpv3 test
import sys
_extract_neighbor_data((sys.argv[1], sys.argv[2], None, True))
asyncio.run(_extract_neighbor_data((sys.argv[1], sys.argv[2], None, True)))
print(repr(_neighdata))
@@ -430,14 +467,14 @@ def list_info(parms, requestedparameter):
results.add(_api_sanitize_string(candidate))
return [msg.ChildCollection(x + suffix) for x in util.natural_sort(results)]
def _handle_neighbor_query(pathcomponents, configmanager):
async def _handle_neighbor_query(pathcomponents, configmanager):
choices, parms, listrequested, childcoll = _parameterize_path(
pathcomponents)
if not childcoll: # this means it's a single entry with by-peerid
# guaranteed
if (parms['by-peerid'] not in _neighbypeerid and
_neighbypeerid.get('!!vintage', 0) < util.monotonic_time() - 60):
for x in update_neighbors(configmanager, retexc=True):
async for x in update_neighbors(configmanager, retexc=True):
if isinstance(x, Exception):
raise x
if parms['by-peerid'] not in _neighbypeerid:
@@ -448,9 +485,9 @@ def _handle_neighbor_query(pathcomponents, configmanager):
if listrequested not in multi_selectors | single_selectors:
raise exc.NotFoundException('{0} is not found'.format(listrequested))
if 'by-switch' in parms:
update_switch_data(parms['by-switch'], configmanager, retexc=True)
await update_switch_data(parms['by-switch'], configmanager, retexc=True)
else:
for x in update_neighbors(configmanager, retexc=True):
async for x in update_neighbors(configmanager, retexc=True):
if isinstance(x, Exception):
raise x
return list_info(parms, listrequested)
+125 -105
View File
@@ -17,7 +17,7 @@
# This provides the implementation of locating MAC addresses on ethernet
# switches. It is, essentially, a port of 'MacMap.pm' to confluent.
# However, there are enhancements.
# For one, each switch interrogation is handled in an eventlet 'thread'
# For one, each switch interrogation is handled in an coroutine
# For another, MAC addresses are checked in the dictionary on every
# switch return, rather than waiting for all switches to check in
# (which makes it more responsive when there is a missing or bad switch)
@@ -42,29 +42,26 @@ if __name__ == '__main__':
import confluent.config.configmanager as cfm
import confluent.snmputil as snmp
import asyncio
from confluent.networking.lldp import detect_backend, _handle_neighbor_query, get_fingerprint
from confluent.networking.netutil import get_switchcreds, list_switches, get_portnamemap
import eventlet.green.select as select
import eventlet.green.socket as socket
import socket
import confluent.collective.manager as collective
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages as msg
import confluent.noderange as noderange
import confluent.tasks as tasks
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
import fcntl
import eventlet
import eventlet.semaphore
import msgpack
import random
import re
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
noaffluent = set([])
@@ -125,9 +122,9 @@ def _namesmatch(switchdesc, userdesc):
return True
return False
def _map_switch(args):
async def _map_switch(args):
try:
return _map_switch_backend(args)
return await _map_switch_backend(args)
except (UnicodeError, socket.gaierror):
log.log({'error': "Cannot resolve switch '{0}' to an address".format(
args[0])})
@@ -152,34 +149,44 @@ def _nodelookup(switch, ifname):
return _switchportmap[switch][portdesc]
return None
def _fast_map_switch(args):
switch, password, user, cfgm = args
async def _fast_map_switch(args):
switch, password, user, cfgm = args[:4]
macdata = None
kv = util.TLSCertVerifier(cfgm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
backend = detect_backend(switch, kv)
backend = await detect_backend(switch, kv)
if backend == 'affluent':
return _affluent_map_switch(switch, password, user, cfgm, macdata)
return await _affluent_map_switch(switch, password, user, cfgm, macdata)
elif backend == 'nxapi':
return _nxapi_map_switch(switch, password, user, cfgm)
return await _nxapi_map_switch(switch, password, user, cfgm)
elif backend == 'srlinux':
return await _srlinux_map_switch(switch, password, user, cfgm)
raise Exception("No fast backend match")
def _nxapi_map_switch(switch, password, user, cfgm):
async def _srlinux_map_switch(switch, password, user, cfgm):
cli = srlinux.SRLinuxClient(switch, user, password, cfgm)
await cli.login()
mt = await cli.get_mac_table()
_macsbyswitch[switch] = mt
_fast_backend_fixup(mt, switch)
async def _nxapi_map_switch(switch, password, user, cfgm):
cli = nxapi.NxApiClient(switch, user, password, cfgm)
mt = cli.get_mac_table()
await cli.login()
mt = await cli.get_mac_table()
_macsbyswitch[switch] = mt
_fast_backend_fixup(mt, switch)
def _affluent_map_switch(switch, password, user, cfgm, macs):
async def _affluent_map_switch(switch, password, user, cfgm, macs):
if not macs:
kv = util.TLSCertVerifier(cfgm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
wc = webclient.SecureHTTPConnection(
wc = webclient.WebConnection(
switch, 443, verifycallback=kv, timeout=5)
wc.set_basic_credentials(user, password)
macs, retcode = wc.grab_json_response_with_status('/affluent/macs/by-port')
macs, retcode = await wc.grab_json_response_with_status('/affluent/macs/by-port')
if retcode != 200:
raise Exception("No affluent detected")
_macsbyswitch[switch] = macs
@@ -213,17 +220,18 @@ def _fast_backend_fixup(macs, switch):
else:
_nodesbymac[mac] = (nodename, nummacs)
def _offload_map_switch(switch, password, user):
async def _offload_map_switch(switch, password, user, privprotocol=None):
if _offloader is None:
_start_offloader()
await _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),
use_bin_type=True))
_offloader.stdin.flush()
result = _offloadevts[evtid].wait()
_offloadevts[evtid] = asyncio.get_event_loop().create_future()
_offloader.stdin.write(msgpack.packb((evtid, switch, password, user, privprotocol),
use_bin_type=True))
#_offloader.stdin.flush()
await _offloader.stdin.drain()
result = await _offloadevts[evtid]
del _offloadevts[evtid]
if len(result) == 2:
if result[0] == 1:
@@ -234,36 +242,36 @@ def _offload_map_switch(switch, password, user):
def _start_offloader():
async def _start_offloader():
global _offloader
_offloader = subprocess.Popen(
[sys.executable, __file__, '-o'], bufsize=0, stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
fl = fcntl.fcntl(_offloader.stdout.fileno(), fcntl.F_GETFL)
fcntl.fcntl(_offloader.stdout.fileno(),
fcntl.F_SETFL, fl | os.O_NONBLOCK)
eventlet.spawn_n(_recv_offload)
eventlet.sleep(0)
#_offloader = subprocess.Popen(
# [sys.executable, __file__, '-o'], bufsize=0, stdin=subprocess.PIPE,
# stdout=subprocess.PIPE)
_offloader = await asyncio.subprocess.create_subprocess_exec(sys.executable, __file__, '-o', stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE)
#fl = fcntl.fcntl(_offloader.stdout.fileno(), fcntl.F_GETFL)
#fcntl.fcntl(_offloader.stdout.fileno(),
# fcntl.F_SETFL, fl | os.O_NONBLOCK)
asyncio.get_event_loop().create_task(_recv_offload())
def _recv_offload():
async def _recv_offload():
try:
upacker = msgpack.Unpacker(encoding='utf8')
except TypeError:
upacker = msgpack.Unpacker(raw=False, strict_map_key=False)
instream = _offloader.stdout.fileno()
#instream = _offloader.stdout.fileno()
while True:
select.select([_offloader.stdout], [], [])
upacker.feed(os.read(instream, 128))
datum = await _offloader.stdout.read(512)
upacker.feed(datum)
for result in upacker:
if result[0] not in _offloadevts:
print("Uh oh, unexpected event id... " + repr(result))
continue
_offloadevts[result[0]].send(result[1:])
eventlet.sleep(0)
_offloadevts[result[0]].set_result(result[1:])
await asyncio.sleep(0)
def _map_switch_backend(args):
async def _map_switch_backend(args):
"""Manipulate portions of mac address map relevant to a given switch
"""
@@ -280,16 +288,15 @@ 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:
return _fast_map_switch(args)
return await _fast_map_switch(args)
except exc.PubkeyInvalid:
log.log({'error': 'While trying to gather ethernet mac addresses '
'from {0}, the TLS certificate failed validation. '
@@ -297,8 +304,8 @@ def _map_switch_backend(args):
'expected due to reinstall or new certificate'.format(switch)})
except Exception as e:
pass
mactobridge, ifnamemap, bridgetoifmap = _offload_map_switch(
switch, password, user)
mactobridge, ifnamemap, bridgetoifmap = await _offload_map_switch(
switch, password, user, privprotocol)
maccounts = {}
bridgetoifvalid = False
for mac in mactobridge:
@@ -367,9 +374,9 @@ def _map_switch_backend(args):
_nodesbymac[mac] = (nodename, maccounts[ifname])
_macsbyswitch[switch] = newmacs
def _snmp_map_switch_relay(rqid, switch, password, user):
async def _snmp_map_switch_relay(rqid, switch, password, user, privprotocol=None):
try:
res = _snmp_map_switch(switch, password, user)
res = await _snmp_map_switch(switch, password, user, privprotocol)
payload = msgpack.packb((rqid,) + res, use_bin_type=True)
try:
sys.stdout.buffer.write(payload)
@@ -383,6 +390,7 @@ def _snmp_map_switch_relay(rqid, switch, password, user):
except AttributeError:
sys.stdout.write(payload)
except Exception as e:
import traceback
payload = msgpack.packb((rqid, 2, str(e)), use_bin_type=True)
try:
sys.stdout.buffer.write(payload)
@@ -391,12 +399,12 @@ def _snmp_map_switch_relay(rqid, switch, password, user):
finally:
sys.stdout.flush()
def _snmp_map_switch(switch, password, user):
async def _snmp_map_switch(switch, password, user, privprotocol=None):
haveqbridge = False
mactobridge = {}
conn = snmp.Session(switch, password, user)
ifnamemap = get_portnamemap(conn)
for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
conn = snmp.Session(switch, password, user, privacy_protocol=privprotocol)
ifnamemap = await get_portnamemap(conn)
async for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
haveqbridge = True
oid, bridgeport = vb
if not bridgeport:
@@ -408,7 +416,7 @@ def _snmp_map_switch(switch, password, user):
)
mactobridge[macaddr] = int(bridgeport)
if not haveqbridge:
for vb in conn.walk('1.3.6.1.2.1.17.4.3.1.2'):
async for vb in conn.walk('1.3.6.1.2.1.17.4.3.1.2'):
oid, bridgeport = vb
if not bridgeport:
continue
@@ -420,15 +428,15 @@ def _snmp_map_switch(switch, password, user):
vlanstocheck = set([])
try:
#ciscoiftovlanmap = {}
for vb in conn.walk('.1.3.6.1.4.1.9.9.68.1.2.2.1.2'):
async for vb in conn.walk('.1.3.6.1.4.1.9.9.68.1.2.2.1.2'):
vlanstocheck.add(vb[1])
#ciscotrunktovlanmap = {}
for vb in conn.walk('.1.3.6.1.4.1.9.9.46.1.6.1.1.5'):
async for vb in conn.walk('.1.3.6.1.4.1.9.9.46.1.6.1.1.5'):
vlanstocheck.add(vb[1])
except Exception:
# We might have crashed snmp on a non-cisco switch
# in such a case, delay 8 seconds to allow recovery to complete
eventlet.sleep(8)
await asyncio.sleep(8)
if not vlanstocheck:
vlanstocheck.add(None)
bridgetoifmap = {}
@@ -440,7 +448,7 @@ def _snmp_map_switch(switch, password, user):
if not isinstance(password, str):
password = password.decode('utf8')
conn = snmp.Session(switch, '{}@{}'.format(password, vlan))
for vb in conn.walk('1.3.6.1.2.1.17.1.4.1.2'):
async for vb in conn.walk('1.3.6.1.2.1.17.1.4.1.2'):
bridgeport, ifidx = vb
bridgeport = int(str(bridgeport).rsplit('.', 1)[1])
try:
@@ -455,13 +463,13 @@ def _snmp_map_switch(switch, password, user):
switchbackoff = 30
def find_nodeinfo_by_mac(mac, configmanager):
async def find_nodeinfo_by_mac(mac, configmanager):
now = util.monotonic_time()
if vintage and (now - vintage) < 90 and mac in _nodesbymac:
return _nodesbymac[mac][0], {'maccount': _nodesbymac[mac][1]}
# do not actually sweep switches more than once every 30 seconds
# however, if there is an update in progress, wait on it
for _ in update_macmap(configmanager,
async for _ in update_macmap(configmanager,
vintage and (now - vintage) < switchbackoff):
if mac in _nodesbymac:
return _nodesbymac[mac][0], {'maccount': _nodesbymac[mac][1]}
@@ -471,10 +479,10 @@ def find_nodeinfo_by_mac(mac, configmanager):
return None, {'maccount': 0}
mapupdating = eventlet.semaphore.Semaphore()
mapupdating = asyncio.Lock()
def update_macmap(configmanager, impatient=False):
async def update_macmap(configmanager, impatient=False):
"""Interrogate switches to build/update mac table
Begin a rebuild process. This process is a generator that will yield
@@ -484,28 +492,28 @@ def update_macmap(configmanager, impatient=False):
"""
if mapupdating.locked():
while mapupdating.locked():
eventlet.sleep(1)
await asyncio.sleep(1)
yield None
return
if impatient:
return
completions = _full_updatemacmap(configmanager)
for completion in completions:
async for completion in completions:
try:
yield completion
except GeneratorExit:
# the calling function has stopped caring, but we want to finish
# the sweep, background it
eventlet.spawn_n(_finish_update, completions)
tasks.spawn(_finish_update(completions))
raise
def _finish_update(completions):
for _ in completions:
async def _finish_update(completions):
async for _ in completions:
pass
def _full_updatemacmap(configmanager):
async def _full_updatemacmap(configmanager):
global vintage
global _apimacmap
global _macmap
@@ -514,7 +522,7 @@ def _full_updatemacmap(configmanager):
global _macsbyswitch
global switchbackoff
start = util.monotonic_time()
with mapupdating:
async with mapupdating:
vintage = util.monotonic_time()
# Clear all existing entries
_macmap = {}
@@ -579,10 +587,12 @@ def _full_updatemacmap(configmanager):
if switch not in switches:
del _macsbyswitch[switch]
switchauth = get_switchcreds(configmanager, switches)
pool = GreenPool(64)
for ans in pool.imap(_map_switch, switchauth):
vintage = util.monotonic_time()
yield ans
#pool = GreenPool(64)
tsks = []
for sa in switchauth:
tsks.append(_map_switch(sa))
for tsk in asyncio.as_completed(tsks):
yield await tsk
_apimacmap = _macmap
endtime = util.monotonic_time()
duration = endtime - start
@@ -599,20 +609,20 @@ def _dump_locations(info, macaddr, nodename=None):
portinfo = []
for location in info:
portinfo.append({'switch': location[0],
'port': location[1], 'macsonport': location[2]})
'port': location[1], 'macsonport': location[2]})
retdata['ports'] = sorted(portinfo, key=lambda x: x['macsonport'],
reverse=True)
yield msg.KeyValueData(retdata)
def handle_api_request(configmanager, inputdata, operation, pathcomponents):
async def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if operation == 'retrieve':
return handle_read_api_request(pathcomponents, configmanager)
return await handle_read_api_request(pathcomponents, configmanager)
if (operation in ('update', 'create') and
pathcomponents == ['networking', 'macs', 'rescan']):
if inputdata != {'rescan': 'start'}:
raise exc.InvalidArgumentException('Input must be rescan=start')
eventlet.spawn_n(rescan, configmanager)
tasks.spawn(rescan(configmanager))
return [msg.KeyValueData({'rescan': 'started'})]
raise exc.NotImplementedException(
'Operation {0} on {1} not implemented'.format(
@@ -630,7 +640,7 @@ def get_node_fingerprints(nodename, configmanager):
_namesmatch)
def handle_read_api_request(pathcomponents, configmanager):
async def handle_read_api_request(pathcomponents, configmanager):
# TODO(jjohnson2): discovery core.py api handler design, apply it here
# to make this a less tangled mess as it gets extended
if len(pathcomponents) == 1:
@@ -641,7 +651,7 @@ def handle_read_api_request(pathcomponents, configmanager):
return [msg.ChildCollection(x + '/')
for x in list_switches(configmanager)]
else:
return _handle_neighbor_query(pathcomponents[2:], configmanager)
return await _handle_neighbor_query(pathcomponents[2:], configmanager)
elif len(pathcomponents) == 2:
if pathcomponents[-1] == 'macs':
return [msg.ChildCollection(x) for x in (# 'by-node/',
@@ -726,31 +736,35 @@ def dump_macinfo(macaddr):
return _dump_locations(info, macaddr, _nodesbymac.get(macaddr, (None,))[0])
def rescan(cfg):
for _ in update_macmap(cfg):
async def rescan(cfg):
async for _ in update_macmap(cfg):
pass
async def get_stdin_reader():
cloop = asyncio.get_event_loop()
reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(reader)
await cloop.connect_read_pipe(lambda: protocol, sys.stdin)
return reader
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '-o':
try:
upacker = msgpack.Unpacker(encoding='utf8')
except TypeError:
upacker = msgpack.Unpacker(raw=False, strict_map_key=False)
currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
async def offloader_main():
try:
upacker = msgpack.Unpacker(encoding='utf8')
except TypeError:
upacker = msgpack.Unpacker(raw=False, strict_map_key=False)
#currfl = fcntl.fcntl(sys.stdin.fileno(), fcntl.F_GETFL)
#fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl | os.O_NONBLOCK)
sreader = await get_stdin_reader()
while True:
data = await sreader.read(512)
upacker.feed(data)
for cmd in upacker:
tasks.spawn(_snmp_map_switch_relay(*cmd))
sys.exit(0)
while True:
r = select.select([sys.stdin], [], [])
try:
upacker.feed(sys.stdin.buffer.read())
except AttributeError:
upacker.feed(sys.stdin.read())
for cmd in upacker:
eventlet.spawn_n(_snmp_map_switch_relay, *cmd)
sys.exit(0)
async def test_main():
cg = cfm.ConfigManager(None)
for res in update_macmap(cg):
async for res in update_macmap(cg):
print("map has updated")
if len(sys.argv) > 1:
print(repr(_macmap[sys.argv[1]]))
@@ -762,3 +776,9 @@ if __name__ == '__main__':
print(repr(_macmap))
print("switch to fdb lookup table: -------------------")
print(repr(_macsbyswitch))
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '-o':
asyncio.run(offloader_main())
sys.exit(0)
asyncio.run(test_main())
@@ -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
@@ -69,10 +72,10 @@ def list_switches(configmanager):
return util.natural_sort(switches)
def get_portnamemap(conn):
async def get_portnamemap(conn):
ifnamemap = {}
havenames = False
for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
async for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
ifidx, ifname = vb
if not ifname:
continue
@@ -80,7 +83,7 @@ def get_portnamemap(conn):
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
if not havenames:
for vb in conn.walk('1.3.6.1.2.1.2.2.1.2'):
async for vb in conn.walk('1.3.6.1.2.1.2.2.1.2'):
ifidx, ifname = vb
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
+31 -26
View File
@@ -1,8 +1,9 @@
import confluent.util as util
import time
import eventlet
webclient = eventlet.import_patched('pyghmi.util.webclient')
import aiohmi.util.webclient as webclient
_healthmap = {
'normal': 'ok',
@@ -87,40 +88,42 @@ class NxApiClient:
self.password = self.password.decode()
except Exception:
pass
self.wc = webclient.SecureHTTPConnection(switch, port=443, verifycallback=cv)
self.login()
self.wc = webclient.WebConnection(switch, port=443, verifycallback=cv)
self.logged = False
def login(self):
async def login(self):
payload = {'aaaUser':
{'attributes':
{'name': self.user,
'pwd': self.password}}}
rsp = self.wc.grab_json_response_with_status('/api/mo/aaaLogin.json', payload)
rsp = await self.wc.grab_json_response_with_status('/api/mo/aaaLogin.json', payload)
if rsp[1] != 200:
raise Exception("Failed authenticating")
rsp = rsp[0]
self.authtoken = rsp['imdata'][0]['aaaLogin']['attributes']['token']
self.wc.cookies['Apic-Cookie'] = self.authtoken
self.wc.cookies.update_cookies({'APIC-cookie': self.authtoken})
self.logged = True
def get_firmware(self):
async def get_firmware(self):
firmdata = {}
for imdata in self.grab_imdata('/api/mo/sys/showversion.json'):
async for imdata in self.grab_imdata('/api/mo/sys/showversion.json'):
attrs = imdata['sysmgrShowVersion']['attributes']
firmdata['NX-OS'] = {'version': attrs['nxosVersion'], 'date': attrs['nxosCompileTime']}
firmdata['BIOS'] = {'version': attrs['biosVersion'], 'date': attrs['biosCompileTime']}
return firmdata
def get_sensors(self):
async def get_sensors(self):
sensedata = []
for imdata in self.grab_imdata('/api/mo/sys/ch.json?rsp-subtree=full'):
async for imdata in self.grab_imdata('/api/mo/sys/ch.json?rsp-subtree=full'):
hwinfo = imdata['eqptCh']['children']
for component in hwinfo:
add_sensedata(component, sensedata)
return sensedata
for sd in sensedata:
yield sd
def get_health(self):
async def get_health(self):
healthdata = {'health': 'ok', 'sensors': []}
for sensor in self.get_sensors():
async for sensor in self.get_sensors():
currhealth = sensor.get('health', 'ok')
if currhealth != 'ok':
healthdata['sensors'].append(sensor)
@@ -130,9 +133,9 @@ class NxApiClient:
healthdata['health'] = 'warning'
return healthdata
def get_inventory(self):
async def get_inventory(self):
invdata = []
for imdata in self.grab_imdata('/api/mo/sys/ch.json?rsp-subtree=full'):
async for imdata in self.grab_imdata('/api/mo/sys/ch.json?rsp-subtree=full'):
hwinfo = imdata['eqptCh']
chattr = hwinfo['attributes']
invinfo = {'name': 'System', 'present': True}
@@ -160,30 +163,32 @@ class NxApiClient:
invdata.append(invinfo)
return invdata
def grab(self, url, cache=True, retry=True):
async def grab(self, url, cache=True, retry=True):
if not self.logged:
await self.login()
if cache is True:
cache = 1
if cache:
if url in self.cachedurls:
if self.cachedurls[url][1] > time.monotonic() - cache:
return self.cachedurls[url][0]
rsp = self.wc.grab_json_response_with_status(url)
rsp = await self.wc.grab_json_response_with_status(url)
if rsp[1] == 403 and retry:
self.login()
return self.grab(url, cache, False)
await self.login()
return await self.grab(url, cache, False)
if rsp[1] != 200:
raise Exception("Error making request")
self.cachedurls[url] = rsp[0], time.monotonic()
return rsp[0]
def grab_imdata(self, url):
response = self.grab(url)
async def grab_imdata(self, url):
response = await self.grab(url)
for imdata in response['imdata']:
yield imdata
def get_mac_table(self):
async def get_mac_table(self):
macdict = {}
for macinfo in self.grab_imdata('/api/mo/sys/mac/table.json?rsp-subtree=full'):
async for macinfo in self.grab_imdata('/api/mo/sys/mac/table.json?rsp-subtree=full'):
mactable = macinfo['l2MacAddressTable']['children']
for macent in mactable:
mace = macent['l2MacAddressEntry']['attributes']
@@ -195,9 +200,9 @@ class NxApiClient:
return macdict
def get_lldp(self):
async def get_lldp(self):
lldpbyport = {}
for lldpimdata in self.grab_imdata('/api/mo/sys/lldp/inst.json?rsp-subtree=full'):
async for lldpimdata in self.grab_imdata('/api/mo/sys/lldp/inst.json?rsp-subtree=full'):
lldpdata = lldpimdata['lldpInst']['children']
for lldpinfo in lldpdata:
if 'lldpIf' not in lldpinfo:

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