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

Compare commits

...

672 Commits

Author SHA1 Message Date
Jarrod Johnson 18b6398c64 Fix XCC discovery issues
A couple of issues could occur during discover that should be
stepped over.
2020-01-23 08:44:17 -05:00
Jarrod Johnson 47b68e4258 Fix dispatch of redfish and tsmsol
redfish and tsmsol was not properly marked as
needing dispatch.
2020-01-22 15:04:43 -05:00
Jarrod Johnson 79b6d099ab Fix attribute sort with python 3
For attributes without a sortid, sort was failing with python3
2020-01-22 14:24:58 -05:00
Jarrod Johnson f1e1d9804a Use python3 for Debian and Ubuntu except for wheezy 2020-01-14 09:52:16 -05:00
Jarrod Johnson 546296ce71 Have the get_webclient always return two elements
Some scenario existed where the function could return
None and this could break code expectations that it
always returned a tuple and the first value needs
to be checked for None.
2020-01-08 10:17:06 -05:00
Jarrod Johnson 954b2dd15c Fix minor formatting issues in source 2020-01-07 09:38:40 -05:00
Jarrod Johnson a7b11d1e15 Fix closing ipmi session when not mapped
When not mapped, a noisy KeyError was occuring.  Handle
it to avoid cluttering log if the work is not needed.
2019-12-10 14:38:12 -05:00
Jarrod Johnson 3660cf18cc Fix a number of issues
For one, there were still bytes v. str for python3 issues in the
certificate exception and credential handling for smm and generic
credential lookup.

There was a python2-ism in lldp that needed to be made 2/3 agnostic
with ord() of a 'bytes' member, converting to bytearray for
normalized behavior.

The discovery core had an issue with chained smms where a set
was used which cannot take a dict, and so it is converted to a list.

If a temporary password is used but the user did not provide a permanent
password that is viable, make the error more explicit.
2019-12-10 11:54:24 -05:00
Jarrod Johnson 078afaf7d7 Make SLP more tolerant of malformed data
Unrelated devices may present invalid data
to SLP queries.  Have these conditions better tolerated.
2019-12-06 11:24:30 -05:00
Jarrod Johnson 3ab1eb7f01 Add words to nodefirmware man page
ipmi versus redfish produces some different behaviors, document
some examples in the man page.
2019-12-04 10:14:23 -05:00
Jarrod Johnson fbfdc9322b Raise errors during connection
This improves error behavior when login action
encounters an error.
2019-12-04 10:06:18 -05:00
Jarrod Johnson ef68656bde Fix nodestorage handling of error
Error messages were not properly handled if specific nodes had issues.
Properly provide the error messages.

Additionally, clarify a common uselessly vague error.
2019-12-03 10:04:34 -05:00
Jarrod Johnson 0e04b853f6 Fix common usage of noderange alerts
This fixes the usual circumstance of not having keyed input data.
2019-11-21 16:42:03 -05:00
Jarrod Johnson 63256e37b6 Remove some uses of xrange
Not friendly to python 3
2019-11-21 16:03:48 -05:00
Jarrod Johnson a1b81e978b Pull in websocket client dependency
This enables tsmsol plugin
2019-11-20 15:25:11 -05:00
Jarrod Johnson 06c53e9d89 Make the websocket module optional
Have tsmsol gracefully degrade for Wheezy, where
it is not relevant to that platform.
2019-11-20 13:53:16 -05:00
Jarrod Johnson ab0168b2ec Update man page for nodesupport
Correct typo and clarify that plugins may customize
filenames.
2019-11-20 10:48:08 -05:00
Jarrod Johnson 473bb39c54 Fix packaging omission of console plugins 2019-11-20 10:04:38 -05:00
Jarrod Johnson a48ecd8c78 Add hardware and firmware info to CNOS
CNOS devices now have nodefirmware and nodeinventory.
2019-11-15 16:08:28 -05:00
Jarrod Johnson 30398fc434 More explicit follower health checking
Be more careful about collective member health assessment,
explicitly closing a follower when it has failure and prefacing
collective commands with a check prior to performing the actual
changes.
2019-11-11 10:49:25 -05:00
Jarrod Johnson 0ed7e4eac3 Merge branch 'master' of github.com:jjohnson42/confluent 2019-11-07 11:26:04 -05:00
Jarrod Johnson 0d2999c7a8 Properly relay errors on console start
Rather than always saying console.method is not set, relay
the str of the exception to give a more accurate info.
2019-11-07 11:25:49 -05:00
Jarrod Johnson 75010ac6f0 Guard against accidental node duplication
If two nodes have same bmc, block one of them to prevent massive
fluctuation back and forth.
2019-11-07 11:10:26 -05:00
Jarrod Johnson a748cc3032 Remove incorrect check for HOSTUNREACH
This seems incorrect and further redundant with other portions
of the code.  This was causing a 'has no len()' error when encountered.
2019-11-05 13:28:12 -05:00
Jarrod Johnson 28bb143880 Recognize drive in rebuilding state
The rebuilding state was not recognized, add that state.
2019-11-04 14:16:02 -05:00
Jarrod Johnson 4b6899d4af Mark node as 'dirty' if it's group is removed
Group removal would end up with the on-disk copy not reflecting
the node membership, even though in-memory copy is fine.
Fix by adding the missing dirty mark to the entry.
2019-11-04 13:43:29 -05:00
Jarrod Johnson a175fd7345 Allow autosuffix to change passed filename
The resultant file may deviate and thus the chown
must have the updated name.
2019-10-29 16:06:51 -04:00
Jarrod Johnson 12bd473c0d Remove stray debug output 2019-10-29 15:50:48 -04:00
Jarrod Johnson a967c224fe Add TSMSOL plugin
This enables ipmi-free access to TSM SOL console
2019-10-29 15:20:40 -04:00
Jarrod Johnson 72a5f37232 Fix mid-line command completion
Use COMP_CWORD instead of -1 to dynamically decide where to
do completion and what candidates to offer
2019-10-24 15:27:17 -04:00
Jarrod Johnson 5c081a8e2c Remove SYSV init script from RPM packages
All supported RPM distros are systemd now...
2019-10-24 12:16:17 -04:00
Jarrod Johnson c0b69f64b8 Error when noderange tries to use secret
secret. cannot be used in expressions, provide
an error in such a case.
2019-10-24 12:04:47 -04:00
Jarrod Johnson f7fe38f498 Fix problem with reassign and TSM 2019-10-24 10:29:58 -04:00
Jarrod Johnson 5ae949b4eb Add UUID to TSM discovery
The UUID is now available in redfish,
use that to get the UUID.
2019-10-24 10:23:59 -04:00
Jarrod Johnson cec6918c1a Support unsupported bad disks 2019-10-23 17:02:37 -04:00
Jarrod Johnson 3df7a36c48 Fix a number of issues with maxnodes
Some commands did not function correctly
after modifications.
2019-10-23 16:47:42 -04:00
Jarrod Johnson 559e96f7ff Fix location of calling the noderange check 2019-10-23 16:27:18 -04:00
Jarrod Johnson 6f568919e1 Correct instance callout
The wrong value was called out
2019-10-23 15:57:16 -04:00
Jarrod Johnson c441739f68 Try a less jarring exit strategy
Have client not provide a developer looking trace on abort.
2019-10-23 14:41:57 -04:00
Jarrod Johnson a43361bb98 Amend the prompt for single node prompting
Remove the trailing slash
2019-10-23 14:38:45 -04:00
Jarrod Johnson 620263db3e Add maxnodes argument to potentially risky commands
This uses the client maxnodes check to double check.  Useful
for clients that want to sanity check unexpectedly large numbers of
nodes.
2019-10-23 14:20:40 -04:00
Jarrod Johnson dd096104cc Provide mechanism to prompt on large noderanges
Some commands can add options to prompt, and can have that option be
only activated on noderanges over a
certain size.
2019-10-22 15:49:09 -04:00
Jarrod Johnson 191ae762ab Use autosuffix feature of pyghmi
This produces output that is more consistent with support by vendors.
2019-10-22 15:17:31 -04:00
Jarrod Johnson f866b7379c Always relax minimum password interval on auto change
The preconfig could interfere with normal operations.
2019-10-22 10:51:41 -04:00
Jarrod Johnson aab6160d49 Fix compatibility between new and old TSM firmware 2019-10-21 16:20:54 -04:00
Jarrod Johnson e1dffe7c3a Fix compatibility with newer TSM firmware
TSM stopped accepting url encoded, switch to json.  This should work
for older and newer firmware.
2019-10-21 14:39:27 -04:00
Jarrod Johnson 97ca6dc48e Provide more detail on leader when leader is lost 2019-10-21 13:55:43 -04:00
Jarrod Johnson 1d59e1da8c Fix unicode issues in HTML api explorer 2019-10-17 15:36:51 -04:00
Jarrod Johnson 7e6b4fa4d0 Fix HTML view with unicode 2019-10-17 11:25:03 -04:00
Jarrod Johnson 186e89cd87 Fix handling autosense with python3 2019-10-17 09:47:46 -04:00
Jarrod Johnson 73d6511d9e Use python3 explicitly for building source
The source distrobution on CentOS7 does not work well
with python2.  Using python3 to build the dist for python2
and python3 fixes issues with extra data being missed in
packaging.
2019-10-16 13:26:15 -04:00
Jarrod Johnson 065ee48325 Remove non-ascii from man page 2019-10-15 14:00:03 -04:00
Jarrod Johnson 4a35cc25f0 Fix TSM setup with python3 and custom credentials 2019-10-15 11:19:04 -04:00
Jarrod Johnson ceced11ae8 Fix python3 compatibility issue 2019-10-14 16:28:32 -04:00
Jarrod Johnson 62084b3605 More fixes for python3/2 compatibility 2019-10-14 15:22:44 -04:00
Jarrod Johnson a84b88e269 Fix mistake in the expression change 2019-10-14 15:02:45 -04:00
Jarrod Johnson fc626d36ba Fix greenlet 'isAlive'
There is no 'isAlive' in a greenlet.
2019-10-14 13:59:24 -04:00
Jarrod Johnson 606a308046 Fix quorum notification in mixed 2/3 collective 2019-10-14 10:33:13 -04:00
Jarrod Johnson bed0f76dba Fix nodeshell/nodersync python3 changes 2019-10-14 09:37:30 -04:00
Jarrod Johnson 8c4d04a7cd Try utf-8 and fallback to latin1 for 2/3 collective
datetime objects are latin1 encoded from python2.

Not an issue for pure python3 or python2 collectives, but
mitigating problems in the mixed environment
2019-10-11 13:58:30 -04:00
Jarrod Johnson bfbde0f7f3 Fix PXE protocol processing under python3 2019-10-11 13:36:16 -04:00
Jarrod Johnson ba96aea2a6 Fix SR635/SR655 discovery with python3 base 2019-10-11 13:35:46 -04:00
Jarrod Johnson d9f1d6c033 Fix user/password login in python3
The forced bytes of the function was incompatible
with str oriented logic later
2019-10-11 13:21:55 -04:00
Jarrod Johnson 1dac61adca Fix python3 in show user
Showing a user was not working in python3
2019-10-11 13:16:38 -04:00
Jarrod Johnson e008932389 Fix python3 collective leader
rpc needs to be fully binary
2019-10-11 11:11:27 -04:00
Jarrod Johnson b81e5fb3ce Fix python3 issues with noderun and nodeshell 2019-10-11 09:53:01 -04:00
Jarrod Johnson e2a08e7e73 Fix bug in initial recheck scheduling
This was causing rechecktime to not be set correctly.
This caused a trace in python3, but indicates an incorrect
behavior when running under python2
2019-10-11 09:09:42 -04:00
Jarrod Johnson eb48728bc1 Have stringify replace by default
This allows easier handling of invalid decode
data.
2019-10-11 08:57:18 -04:00
Jarrod Johnson c6255fa13b Fix SMM discovery in python3 2019-10-10 17:10:41 -04:00
Jarrod Johnson 2a37d64dc9 Fix XCC discovery in python3
Python3 was picky about the strings, normalize key areas
to strings.
2019-10-10 17:03:57 -04:00
Jarrod Johnson 8101672c3a Fix discovery with python3 2019-10-10 16:49:36 -04:00
Jarrod Johnson 453c344f7f Fix audit log for non root usage with python 3 2019-10-10 16:07:45 -04:00
Jarrod Johnson 9eceda0636 Fix non-root user issue with python3 2019-10-10 16:04:23 -04:00
Jarrod Johnson f28b91499e Fix console replay with formatting
Bold and related items would trigger a problem with python3
2019-10-10 15:01:00 -04:00
Jarrod Johnson 8cab591a8b Add collective member deletion
This allows deletion of a dead member, down to deleting down to non-collective
mode.
2019-10-10 11:30:03 -04:00
Jarrod Johnson 6ce6740b77 Provide deletion of collective member 2019-10-10 08:50:42 -04:00
Jarrod Johnson 8a4a219a14 Fix two more python2 string behavior
In python3, bytes[n] is an int, but not in python2.

Sidestep by doing bytearray() for both, which is consistent
between the two.
2019-10-09 15:06:23 -04:00
Jarrod Johnson a39d45d03c Fix some more python3 issues 2019-10-09 13:42:17 -04:00
Jarrod Johnson bc85d93cf4 More Py3 fixes 2019-10-09 11:20:03 -04:00
Jarrod Johnson 1e963106fe Fix unicode issues with web api 2019-10-09 11:03:30 -04:00
Jarrod Johnson a9f0e345db Another set of python3 fixes 2019-10-08 17:10:56 -04:00
Jarrod Johnson 5353b479d9 More python3 fixes 2019-10-08 13:46:44 -04:00
Jarrod Johnson dbc6747c38 Fix nodebmcreset for python3 2019-10-08 13:43:53 -04:00
Jarrod Johnson 0edd1efe0d Fix python3 dump_keys
When doing format on base64
output, it must be explicitly
coerced into native string
format.
2019-10-08 13:21:34 -04:00
Jarrod Johnson 8e87f5b9e5 Fix python3 issue with base64 and str 2019-10-08 11:20:27 -04:00
Jarrod Johnson 0633b2ca67 Fix syntax error in code 2019-10-08 10:59:44 -04:00
Jarrod Johnson c1953bdad3 Another set of python 3 compatibility
Numerous issues arose, particularly
when participating in a mixed
collective.
2019-10-08 10:45:43 -04:00
Jarrod Johnson 578ba06aa3 Fix python3 problem with octal 2019-10-08 09:06:15 -04:00
Jarrod Johnson 0975881d3b Add monotonic dependency 2019-10-07 17:06:45 -04:00
Jarrod Johnson 8fc3b7c9c0 Implement cross-python collective compat
This enables cross-version compatibility
for a collective.
2019-10-07 15:41:38 -04:00
Jarrod Johnson 6ea6ebd80e Remove superfluous dependency 2019-10-04 16:49:51 -04:00
Jarrod Johnson 744f8899f7 Merge branch 'py3' 2019-10-04 10:57:07 -04:00
Jarrod Johnson 5c288a27dd Have EL8 use python3 for confluent 2019-10-04 10:54:20 -04:00
Jarrod Johnson 74f18d5571 Client side py3 changes 2019-10-04 10:37:48 -04:00
Jarrod Johnson d9be6ae2e9 Close console on disconnect
Fixes leftover console on user initiated disconnect.
2019-10-04 09:57:54 -04:00
Jarrod Johnson 59789bae7d Fix python3 ctypes str usage
In python3, the string is likely to be unicode and incompatible
with the libc function.  If it isn't bytes, force it to be bytes.
2019-10-03 16:06:15 -04:00
Jarrod Johnson 521be5d44d Further Python3 compatibility changes
With this as well as eficompressor and pyghmi updates, things
seem to be in roughly working order
2019-10-03 15:57:56 -04:00
Jarrod Johnson 6fb82bbbad Further Python3 compatibility changes 2019-10-02 11:29:13 -04:00
Jarrod Johnson 90e546bcac Implement a number of py3 compatible adjustments 2019-10-02 08:58:39 -04:00
Jarrod Johnson 147d59cba7 Migrate from PyPAM
PyPAM is no longer part of the distributions.  Closest match is also not in
the distributions and also contains a security problem without an external
patch, so it is pulled in and pull request with copyright and license
intact.
2019-10-01 11:28:43 -04:00
Jarrod Johnson 6a6fd3184e Add missing dependencies for EL8 2019-09-30 10:51:26 -04:00
Jarrod Johnson c532cf9ecf Add EL8 requires adapting to confluent 2019-09-25 15:38:36 -04:00
Jarrod Johnson 8909fb16d6 Change rpm spec to build using python2
This will fix shebang mangling.
2019-09-23 11:11:16 -04:00
Jarrod Johnson 44d6bde3ff Make /usr/bin/env python point to python2
Same as before, more RHEL8 compatibility changes
2019-09-23 11:04:52 -04:00
Jarrod Johnson 56fa13279e Explicitly indicate use of python2
RHEL8 will no longer tolerate implicit use of python.
For now relent to being python2, though ideally one day
it could be either.

Unfortunately, this means once code is ready for python3, we have to
probably implement build time changes for python3 enabled distros
to have different shebangs than python2 distros.
2019-09-23 10:59:00 -04:00
Jarrod Johnson 5008128d57 Add IPv6 support to TSM
TSM firmware fixes enable IPv6, enable our support of it.
2019-09-19 10:02:40 -04:00
Jarrod Johnson ed320f4a17 Add 'check' to permit comparison against current value
For implementing some security policies, it is useful
to check new value against current value.
2019-09-17 09:48:02 -04:00
Jarrod Johnson de8292f6dd Ignore current channel if current channel is disabled
A fluke can cause current channel to be 1 when we are wanting 8.
2019-09-06 13:31:14 -04:00
Jarrod Johnson 8e071a2568 Fix missing import
The smm change omitted a required import.
2019-09-05 12:45:29 -04:00
Jarrod Johnson 7d84d0a021 Add network config to SD635/SD655 discovery
This completes the most critical portion of SD635/SD655 functionality.
2019-09-05 12:10:58 -04:00
Jarrod Johnson c5f75bfa15 Fix slowdown when no password change needed
If the password was pre-provisioned, accelerate the config
phase of tsm bringup.
2019-09-05 11:10:38 -04:00
Jarrod Johnson 99d01d707f Fix incorrect bad auth on freshly changed password
If the database backing the password had changed but cache hadn't updated,
then the cache miss was taken as always meaning no valid login.
2019-09-05 11:09:11 -04:00
Jarrod Johnson 3e1690c860 Add authentication and ipmi configuration to SR635/SR655
Network configuration and the lockout policy remain to be configured.
2019-09-04 16:20:31 -04:00
Jarrod Johnson a3f5630535 Add SR635/SR655 detection
Currently merely detects.  A discover handler will be next step
to populate UUID and do initial setup.
2019-09-03 15:49:27 -04:00
Jarrod Johnson 8d6744947e Extended bmc config support to redfish
Though the underlying library is currenty a stub, bring the
confluent plugin to parity so that nodeconfig works fine
with redfish again.
2019-09-03 11:05:51 -04:00
Jarrod Johnson 21b3c89974 Improve redfish plugin errors and attrib handling
Redfish plugin was not handling the various socket error
behaviors correctly.

Additionally, the attribschanged handler was failing to actually
do anything, so commands would gladly reuse an old redfish
object if it were working.
2019-09-03 10:06:18 -04:00
Jarrod Johnson 9718881c7b Merge branch 'master' of github.com:jjohnson42/confluent 2019-08-30 10:22:59 -04:00
Jarrod Johnson fcbbdc8a8d Add python-pyopenssl and python-dateutil deps
For wheezy, the only supported user wants to
always have collective capability.
Workaround pyghmi missing the dateutil dependency.
2019-08-30 10:22:17 -04:00
Jarrod Johnson f03bb36dbe Fix nodeconfig treatment of bmc attributes
Make bmc behave like other categories/settings.
2019-08-29 15:24:14 -04:00
Jarrod Johnson 97a950b145 Retain XCC IP on discovery if already set
Do not change from DHCP if DHCP is working the way intended.
2019-08-28 09:46:35 -04:00
Jarrod Johnson a0e445d1b1 Force MSS down for forwarded connections
Some endpoints may not be able to fully handle the natural
MSS suggested by MTU.
2019-08-28 09:16:42 -04:00
Jarrod Johnson 65629548c9 Add support for new BMC config capabilities
This enables nodeconfig -r bmc and nodeconfig bmc.<attr> function.
2019-08-27 14:56:19 -04:00
Jarrod Johnson 6cfbf4533c Add support for BMC clear and extended attributes
This wires up new function in pyghmi.
2019-08-27 14:32:16 -04:00
Jarrod Johnson 5794dd7f8c Add CP storage to discovery 2019-08-22 16:15:01 -04:00
Jarrod Johnson 48fa74e7a9 Explicitly use utf8 when encode error encountered
Some platforms do not naturally do utf-8.  This is
pretty much always the answer, so try it if the default
encoding of a platform fails.
2019-08-22 08:57:47 -04:00
Jarrod Johnson 59d5110b8f Also enable ipmi if user sets in console.method 2019-08-20 09:29:25 -04:00
Jarrod Johnson 051d79727b Fix sysvinit
sysvinit lsb function is 'status_of_proc'
2019-08-20 08:59:01 -04:00
Jarrod Johnson 4cdbc7807b Add thinkagile storage to discovery
At least enumerate the ipv4 pieces.
2019-08-19 16:37:14 -04:00
Jarrod Johnson feecee82db Mitigate excessive open web sessions
Back to back discovery testing would exhaust web slots.
Severely mitigate through being more aggressive about logging out.
2019-08-19 10:44:47 -04:00
Jarrod Johnson 395c0d4697 Preserve IPMI state unless user wants otherwise
ALso a draft of navigating a forced password change
in the midst of a pre-hashed password conversion.
2019-08-16 16:33:57 -04:00
Jarrod Johnson a963a8ca35 Handle forced password change and no IPMI for XCC
XCC firmware will start mandating a password change before use.

Additionally, IPMI will be unavailable and will be needed for enabling SMM.

TODO:
-Discover USERID/TempW0rd42 nodes that weren't preconfigged

-Enable IPMI on non-SD530 if hardwaremanagement.method is not redfish.
2019-08-15 16:40:15 -04:00
Jarrod Johnson d95464df6f Add a non-ip network string xmit concept 2019-08-08 16:14:05 -04:00
Jarrod Johnson 3f6e6d4c39 Support unavailable sensors in redfish 2019-08-08 15:02:26 -04:00
Jarrod Johnson c3176ab86a Add blink and identify reporting to redfish 2019-08-05 16:22:50 -04:00
Jarrod Johnson 4dab5fc527 Adapt to RHEL or Debian openssl config locations 2019-08-05 16:16:42 -04:00
Jarrod Johnson 8897842fc4 Fix SMM handler when None bmc
This fixes a common scenario for using fe80 collection
2019-07-26 13:50:59 -04:00
Jarrod Johnson a251a538b0 Improve SMM discovery
SMM discovery behavior has seemingly gotten more picky with time.
First switch to an IPMI-free if the user has custom password.  The
web based approach is much less problematic than SMM IPMI stack in
this context.

If user specifies they want to use default credentials, we have
no choice but to use IPMI.  Omit things and shuffle order of operations
to mitigate problems.  It isn't perfect, but it does work eventually.
2019-07-26 09:25:19 -04:00
Jarrod Johnson 480a747dcf Merge branch 'master' of github.com:jjohnson42/confluent 2019-07-25 13:10:08 -04:00
Jarrod Johnson af025f7304 Present log name when provided in nodeeventlog
Some managers combine logs, allow disambiguation through including in output.
2019-07-25 13:09:52 -04:00
Jarrod Johnson 21edd82177 Extend generic catches in redfish/ipmi
Have it provide more detailed error data at
a glance to short out some debug requirements.
2019-07-25 09:28:55 -04:00
Jarrod Johnson 8641885f86 Fix handling of socket error with neighbor
If a system is half up, a different sort of timeout is given.
Handle this and also preserve the original exception better
if not one of these two.
2019-07-25 08:52:47 -04:00
Jarrod Johnson 64cc2416d1 Fix list argument 2019-07-24 15:37:01 -04:00
Jarrod Johnson 2787e1d862 Present more data about missing entries from assign csv
When doing an assign on csv, present all the missing entries
rather than stopping on the first.
2019-07-23 11:57:35 -04:00
Jarrod Johnson 514a121c15 Print license install errors when provided
When the service gives errors installing keys,
relay them to the user.
2019-07-22 11:21:31 -04:00
Jarrod Johnson 00ce48b046 Fix behavior for bad nodelicense arguments
Correctly show help and exit if unrecognized
parameter.
2019-07-22 09:27:31 -04:00
Jarrod Johnson 44929e7975 Fix printing of unicode to pipe
nodesensors would have a unicode error on pipe output.
2019-07-19 15:36:43 -04:00
Jarrod Johnson da82fef0cb Have nodelist use nodeattrib completion
Since nodelist may also show attribute values, use same completion function.
2019-07-19 14:22:19 -04:00
Jarrod Johnson efcac0b181 Fix nodesensors -n with csv 2019-07-19 14:12:40 -04:00
Jarrod Johnson 46e2f53018 Always use GCM on encrypt
Continue to support read using the old scheme, but even when
an integrity key is available, only use it to aid in decrypting
classic format, and always write in new format.
2019-07-17 08:51:06 -04:00
Jarrod Johnson cf51928b3d Implement GCM and close gap in the HMAC
When generating key material from scratch, skip HMAC and
activate GCM mode.

When using existing CBC/HMAC keys, start covering the IV value
in the HMAC.  For compatibility, HMAC validity is checked with
and without IV.
2019-07-16 18:32:23 -04:00
Jarrod Johnson 151ba2e567 Add error messages to nodeattrib clear
nodeattrib -c was not reporting useful error information.
2019-07-15 13:28:09 -04:00
Jarrod Johnson bc87077397 Fix error handling and consistency in networking
by-port specification was inconsistent and unhelpful in error between macmap and lldp.
2019-07-15 11:07:59 -04:00
Jarrod Johnson a77b65737e Suppress usage on nodefirmware error
It is very bizarre to have the usage output
after an error.
2019-07-12 12:10:22 -04:00
Jarrod Johnson 19c2963cf9 Fix reassign with SMM
SMM validation assumes an earlier connection, fix so that it will accept the certificate
if no certificate expected yet.
2019-07-11 15:55:20 -04:00
Jarrod Johnson a0ea8eeae3 Fix nodediscover support of bmc_gateway
The command expected spaces, but documentation
said underscore, support both old csvs and doc
compliant csvs.
2019-07-11 15:36:31 -04:00
Jarrod Johnson 6ad1ce4df5 Back off concurrent retrieval.
Some BMCs are incapable of handling concurrent requests.
This is a blow particularly to high latency management given
Redfish's tendency to require a lot of resource fetches, but
we don't have a particularly discoverable strategy for knowing
in advance whether an implementation is up for some optimization.
2019-07-11 11:41:42 -04:00
Jarrod Johnson c53264872a Fix scenario with undefined passlength
If passwordcomplexity were requested without a passwordlength,
things would be a problem.
2019-07-11 10:53:43 -04:00
Jarrod Johnson d9f2c0b266 Correct mistake in setting names 2019-07-11 10:40:49 -04:00
Jarrod Johnson d528d45820 Add configuration to opt into password policies. 2019-07-11 10:19:46 -04:00
Jarrod Johnson 4eeac8d71a Explore password evaluation as an option.
Password rules may be relevant to some scenarios.  In such a case, this
can provide guidance if the BMC does not have such a facility or
alternatively provide friendlier warnings than the BMC provides
around shortcomings of the password.
2019-07-10 15:32:47 -04:00
Jarrod Johnson 6cc0eb0797 Add reassign to nodediscover
Allow nodediscover to do 'reassign' to repeat discovery process for a discovered
node, without requiring an additional identifier.
2019-07-09 13:53:30 -04:00
Jarrod Johnson bfd0de1a4a Add expiration to more places 2019-07-09 13:26:48 -04:00
Jarrod Johnson a787ac62c3 Add expiration data from ipmi plugin
When pyghmi provides the information, enrich the return
data.
2019-07-09 12:48:56 -04:00
Jarrod Johnson fd9b4a8650 Add support for expiration data, when available 2019-07-09 10:55:33 -04:00
Jarrod Johnson 373bf3dca7 Remove stray whitespace
Fix formatting mistakes
2019-07-09 10:18:34 -04:00
Jarrod Johnson 0ad0c626c2 Restore password policy set to nodediscover
The shift away from IPMI had omitted the password policy set.
Amend the function and restore it to the sequence of events.
2019-07-08 16:25:07 -04:00
Jarrod Johnson fbc4fc6846 Make unexpected error more specific
Often a usable summary message is obfuscated.  Assume the subject line
is safe to relay, but continue to do a more verbose trace.
2019-07-08 14:28:56 -04:00
Jarrod Johnson 3efc153615 Improve conversion reliability
It was frequent that a token expiration would impact attempt to convert
an account.  Suppress the token based authentication to more reliably
have a fresh login.

Additionally, mitigate chance of exhausting user login limit.

Finally, switch to a generated password for the temporary account.  Should something go awry
between deleting the third-party account and recreating it, this
means the system will have to be reset through OS or F1 menu.  However this is better
than the risk of a well known backdoor account being inadvertently
created.
2019-07-03 11:39:36 -04:00
Jarrod Johnson b7ff093e48 Fix ubuntu package install location 2019-07-02 14:42:45 -04:00
Jarrod Johnson 7275e98039 Remove IPMI specific parameters to user
Users cannot have these settings in redfish
2019-07-01 11:23:28 -04:00
Jarrod Johnson 2b0c50dc23 Refresh connection parameters on retry
If the parameters changing cause a login failure,
take the opportunity to refresh that information to work
in the midst of a rediscover, for example.
2019-07-01 09:15:01 -04:00
Jarrod Johnson 54439d5f18 Fix cause of former 'login process died'
If a session died without calling logged, the confluent plugin
instance would be orphaned.  Detect this situation to trigger a
retry.
2019-07-01 09:05:08 -04:00
Jarrod Johnson 65b4cbe8cc Revised fix for redfish name missing case 2019-06-28 16:02:19 -04:00
Jarrod Johnson c8931ae6e7 Revert "Carry fix for null names to redfish"
This reverts commit 8bbeeafa49.
2019-06-28 16:01:26 -04:00
Jarrod Johnson 083f5c8654 Add python-dateutil rpm dependency 2019-06-28 15:41:17 -04:00
Jarrod Johnson 8bbeeafa49 Carry fix for null names to redfish 2019-06-28 15:14:59 -04:00
Jarrod Johnson 422f210f74 Handle adapters with null names
Under some situations, null names are given
for adapters, apply generic treatment to such
devices.
2019-06-28 15:13:56 -04:00
Jarrod Johnson 2e6029bd2c Add a location.height attribute
This permits user to specify height for either unsupported systems
or to speed up the rackview drawing.
2019-06-28 09:30:53 -04:00
Jarrod Johnson 81c0adbbe3 More python 3 compatibility fixes
Improve more commands and modules to parse and execute under python 3.
2019-06-28 08:51:57 -04:00
Jarrod Johnson 6d5f0cdb16 Make TCP_FASTOPEN opportunistic
Certain Linux-like but not Linux environments fail
at this.
2019-06-27 15:36:27 -04:00
Jarrod Johnson c633286019 Add missing dateutil dependency to apt 2019-06-27 15:31:00 -04:00
Jarrod Johnson ba113d6445 Fix various python2-isms
This is far from a complete python3 support, but it lets a significant volume
of remote commands run under python3
2019-06-27 15:28:59 -04:00
Jarrod Johnson d2efb16c71 Remove unneeded line 2019-06-27 15:21:35 -04:00
Jarrod Johnson 739e302506 Add an example for just disabling password complexity 2019-06-27 15:20:24 -04:00
Jarrod Johnson ae181b7753 Improve nodelicense error messages
Check and exit should the file or directory not exist.
2019-06-26 13:24:24 -04:00
Jarrod Johnson b76b415a6e Update nodelicense man page 2019-06-26 08:45:11 -04:00
Jarrod Johnson ef2b324eed Flush csv output per row
Improve responsiveness of csv output when piping
2019-06-25 11:03:50 -04:00
Jarrod Johnson ffe9606de1 Auto-repair 'login process died' condition
The cause is still unknown, but we should be able to at least
repair automatically.
2019-06-24 16:34:56 -04:00
Jarrod Johnson 8ed2d5a551 Fix confulent2xcat usage text
It was incorrectly referencing ansible.
2019-06-24 14:17:32 -04:00
Jarrod Johnson 3501b3c347 Add state info to all licenses
The all resource was not retrieving the licenses.
2019-06-21 15:52:24 -04:00
Jarrod Johnson e55314d759 Add display of inactive licenses
Inactive licenses are also an issue.
2019-06-21 10:17:52 -04:00
Jarrod Johnson 27410a9b6b Fix discover of XCC
Incorrect module path was indicated.
2019-06-20 10:56:09 -04:00
Jarrod Johnson 2db01746d5 Support deletion of licenses from XCC 2019-06-19 16:35:24 -04:00
Jarrod Johnson 208be0beef Implement nodelicense save
Add ability to save licenses from
the XCC.
2019-06-19 15:43:45 -04:00
Jarrod Johnson d34f8af798 Rework redfish support for user management
The redfish user management api couldn't quite be identical,
adjust to the changes.
2019-06-13 15:02:53 -04:00
Jarrod Johnson b2013e93c5 Mitigate performance impact of oem sensors
The sensormap was being regenerated each time a sensor
was asked for.  Make the sensor map
a more perisstent fixture.
2019-06-12 14:47:28 -04:00
Jarrod Johnson 2a72a6184d Fix confluent-wide pauses during redfish
The lenovo OEM module was unpatched in import and
thus it inflicted a confluent-wide sleep instead of
a patched time.sleep.
2019-06-12 10:16:19 -04:00
Jarrod Johnson 7e4dcfa99c Merge branch 'master' of github.com:jjohnson42/confluent 2019-06-11 11:07:31 -04:00
Jarrod Johnson ee82831370 Do not flag file: as insecure
Locally hosted media should not be considered
insecure.
2019-06-11 11:06:59 -04:00
Jarrod Johnson 0869669ef6 Better isolate system config fault
System configuration fault will now be contained to node.
2019-06-07 16:57:03 -04:00
Jarrod Johnson 6a77a13539 Improve error for unserializable collective data
XML errors turn out to be unserializable.  Catch this general
class of problems and provide a less devastating behavior.

Doing an xml.fromstring('') is an example of how to trigger it.
2019-06-07 16:47:17 -04:00
Jarrod Johnson 56e9a67ef8 Fix boundary issue on oversize file
If the logfile is large enough to cause
a struct.error, just force a size
roll and continue.

Also unconditionally unlock the file.
2019-06-07 15:54:12 -04:00
Jarrod Johnson 52d5eb9876 Add stripsize to the redfish plugin
Storage creation was not passing through the stripsize parameter.
2019-06-07 09:34:29 -04:00
Jarrod Johnson b819a488f1 Mirror hotspare handling from ipmi to redfish
The redfish plugin should handle the same disk information as ipmi plugin.
2019-06-06 15:13:19 -04:00
Jarrod Johnson 3fc31f7332 Add dedicated hot spare information
If an array contains dedicated hot spare, properly
report it alongside the member disks.
2019-06-06 11:37:30 -04:00
Jarrod Johnson 21c3579287 Add setdisk and stripsize
More nodestorage improvements.
2019-06-05 15:54:29 -04:00
Jarrod Johnson 4a094f669e Do not break for zsh users
The completion functionality shall be skipped
unless the shell is bash specifically.
2019-06-05 14:27:08 -04:00
Jarrod Johnson 67eecffd29 Force stripsize to numeric
This should be a numeric value, even if it came in as a string.
2019-06-05 11:22:35 -04:00
Jarrod Johnson e288e8bad5 Add stripsize to the input on volume
Explictly ensure a stripsize key.
2019-06-05 11:16:44 -04:00
Jarrod Johnson a8cad7a70f Add stripsize to API
Allow the caller to select a custom stripsize if desired.
2019-06-04 16:33:10 -04:00
Jarrod Johnson 6de605c298 Switch to python2/3 agnostic lower
The string.lower is not in python3
2019-06-04 16:27:20 -04:00
Jarrod Johnson e09c2ed8eb Support more convoluted detail
Some uefi settings details contain
more tricky detail information.
2019-06-03 15:30:32 -04:00
Jarrod Johnson cd5366e73f Fix nodeconfig for non-ascii choices 2019-05-31 14:47:07 -04:00
Jarrod Johnson 509f8c30d5 Remove use of IPMI in XCC config
The XCC configuration is now entirely
over https and well suited for an ipmi disabled scenario.
2019-05-31 08:50:36 -04:00
Jarrod Johnson c63c8076bb Use https to set network on XCC
This quite nearly completes the removal
of IPMI requirement during bootstrap.
2019-05-29 16:36:13 -04:00
Jarrod Johnson 6800c8055c Implement IPMI-free xcc config (work in progress) 2019-05-29 14:31:39 -04:00
Jarrod Johnson ffc55b1594 Reduce concurrency to redfish targets
Some redfish implementations did
not handle a relatively open ended barrage
of concurrent requests.  Try limitting to 4 concurrent
requests to evaluate how the implementations handle it.
2019-05-20 11:03:26 -04:00
Jarrod Johnson 481342340e Fix potential nodesensors crash
There is a tiny chance that time will tick between two
calls.  This fixes it.
2019-05-16 14:37:18 -04:00
Jarrod Johnson d33c6be758 Fix forwarder over custom https ports 2019-05-16 14:36:39 -04:00
Jarrod Johnson 44f3630cf5 Further amend formatting of nodestorage manual 2019-05-14 13:56:20 -04:00
Jarrod Johnson 3eaba23e6f Fix formatting of nodestorage man page 2019-05-14 13:53:28 -04:00
Jarrod Johnson 5ac0a6e650 Fix raid configuration for operator
Operator was not allowed to create arrays.
2019-05-14 10:42:42 -04:00
Jarrod Johnson 9ac83665c6 Isolate redfish node errors
If code experiences an issue specific to a node, isolate that fault to the node.
2019-05-13 14:37:47 -04:00
Jarrod Johnson 30f9d28c2c Merge branch 'master' of github.com:jjohnson42/confluent 2019-05-13 13:36:17 -04:00
Jarrod Johnson 0168e46f24 Isolate individual node errors in ipmi plugin
If an individual node experiences an unexpected error,
isolate the fallout to that specific node.
2019-05-13 13:35:03 -04:00
Jarrod Johnson 067e99d6ce Merge branch 'master' of github.com:jjohnson42/confluent 2019-05-10 14:56:48 -04:00
Jarrod Johnson ad828e609d Reduce bad default login tries
For an SD530 XCC, we would incur 4 attempts at default:
-To pre-config enable SMM, we try once
-Due to pyghmi auto-degrade, try again as oper
-Then during the actual config phase, try again
-Again, try again as oper

Now with pyghmi opt-out, we will force to try only as admin, eliminating the second try.

The SD530 code will now mark that the default creds failed so that
the config phase will know to skip that.
2019-05-10 14:34:39 -04:00
Jarrod Johnson cc5a5c9972 Fix operator add and delete of nodes
This permits operators to run nodedefine and noderemove.
2019-05-10 13:15:19 -04:00
Jarrod Johnson cd2361b80b Fix nodediscover clear for operators
Operators should be allowed to delete discovery data.
2019-05-10 13:11:04 -04:00
Jarrod Johnson c042583a64 Add support for CSV formatted data
Presume CSV semantics for input.
2019-05-10 11:08:12 -04:00
Jarrod Johnson e32d3cf4cc Add auto-index determination to stats
This allows it to auto-skip over units, for example.
2019-05-10 10:34:56 -04:00
Jarrod Johnson 2b86c878a8 Cleanly handle bad credentials in redfish
Provide a similar experience to the ipmi plugin.
2019-05-07 16:19:30 -04:00
Jarrod Johnson 3564de8c6d Fix web consoles/shells for operators
Operator role needed more permissions to act as expected.
2019-05-07 15:58:34 -04:00
Jarrod Johnson 7b5361a019 Add expressions to Operator role
noderun/nodeshell would not work for operators without this.
2019-05-03 09:06:20 -04:00
Jarrod Johnson 65e1dfcc57 Fix nodesensors with redfish plugin
redfish plugin does not produce the same data as ipmi,
tolerate that difference.
2019-05-02 10:54:15 -04:00
Jarrod Johnson ba039e9e3e Fix nodeeventlog on ipmi devices
Changes for redfish broke against ipmi plugin
2019-05-02 10:21:24 -04:00
Jarrod Johnson a6809aae98 Add Monitor role
Add a monitor role that is only viable for monitoring relevant
tasks.
2019-05-02 10:04:40 -04:00
Jarrod Johnson 4d5bfb13bf Add support for Operator role
Support a reduced privilege user that can still perform
most operations, but cannot modify, delete, or add
users/groups to confluent or to BMCs.
2019-05-01 16:57:15 -04:00
Jarrod Johnson 93e9a54e86 Relay redfish error strings to client
This provides a much better experience than 'unexpected'
error.
2019-05-01 11:46:02 -04:00
Jarrod Johnson 25028c8acc Merge branch '2.2.cme' 2019-05-01 09:18:09 -04:00
Jarrod Johnson 906c671d90 Fix misakes in usergroups
Deletion was incorrect and restore from json did not work.
2019-04-30 16:18:36 -04:00
Jarrod Johnson 8fbd99cf5c Fix misakes in usergroups
Deletion was incorrect and restore from json did not work.
2019-04-30 16:18:12 -04:00
Jarrod Johnson c86ac2885f Fix overly verbose log on client close
When a client would close (e.g. an unathenticated nodelist),
a large trace be logged.  Fix by returning silently in such a case.
2019-04-30 15:27:50 -04:00
Jarrod Johnson 952fa3d022 Add user groups to confluent
This allows a system/ldap group to be used instead of directly
specifying individual authorized users.
2019-04-30 15:27:41 -04:00
Jarrod Johnson 90e0f93d37 Module to assist with advanced user manipulation
Currently holds the logic to ascertain the system groups
for a system user.
2019-04-30 15:27:34 -04:00
Jarrod Johnson d78adc334d Fix overly verbose log on client close
When a client would close (e.g. an unathenticated nodelist),
a large trace be logged.  Fix by returning silently in such a case.
2019-04-30 15:03:55 -04:00
Jarrod Johnson 31f2161b57 Add user groups to confluent
This allows a system/ldap group to be used instead of directly
specifying individual authorized users.
2019-04-30 14:55:54 -04:00
Jarrod Johnson 571a34cba2 Module to assist with advanced user manipulation
Currently holds the logic to ascertain the system groups
for a system user.
2019-04-30 13:23:26 -04:00
Jarrod Johnson 52fa5158f6 Fix display of final bin members in verbose 2019-04-26 16:51:44 -04:00
Jarrod Johnson 907f66ae8b Have the range be more precise on verbose 2019-04-26 16:43:53 -04:00
Jarrod Johnson c8c275f804 Fix indentation error 2019-04-26 16:40:47 -04:00
Jarrod Johnson 7a08fee4b5 Actually fix the verbose range 2019-04-26 16:29:33 -04:00
Jarrod Johnson 36f0d888cd Fix the verbose output boundaries. 2019-04-26 16:17:28 -04:00
Jarrod Johnson 81451a6451 Add options to stats
Implement verbose, text plot, and custom select bins
2019-04-26 16:04:01 -04:00
Jarrod Johnson 02eb195e3f Change topline of script to be consistent 2019-04-25 14:47:20 -04:00
Jarrod Johnson 87e7a90c37 Move stats into the client
stats is good enough to be promoted from prototype to a confluent
client component.
2019-04-25 14:46:18 -04:00
Jarrod Johnson bafc25005f Flesh out stats with arguments 2019-04-25 14:45:47 -04:00
Jarrod Johnson 33c1137ccf Remove use of tmp file in stats 2019-04-25 13:59:15 -04:00
Jarrod Johnson abfeef5a0a Merge branch '2.2' 2019-04-25 13:53:41 -04:00
Jarrod Johnson e81579f414 Add a prototype stats command for CLI commands 2019-04-25 13:53:34 -04:00
Jarrod Johnson 47edb1dbd1 Add a prototype stats command for CLI commands 2019-04-25 13:51:50 -04:00
Jarrod Johnson 6a8cb8deaa Merge branch '2.2' 2019-04-22 13:17:16 -04:00
Jarrod Johnson c6516f9d62 Support redfish event logs 2019-04-22 12:54:49 -04:00
Jarrod Johnson 72448aa0b4 Disable MIB resolution for raw requests
MIB resolution turns out to be rather CPU intensive, and the
current SNMP consumers don't want the resolution anyway.
2019-04-16 08:49:32 -04:00
Jarrod Johnson 6290c169f5 Disable MIB resolution for raw requests
MIB resolution turns out to be rather CPU intensive, and the
current SNMP consumers don't want the resolution anyway.
2019-04-16 08:46:50 -04:00
Jarrod Johnson e5bbd226ff Add completion for attributes in node*attrib
Make some of the tedium of the long attribute names bearable
through tab completion.
2019-04-15 14:01:59 -04:00
Jarrod Johnson 037ed43c70 Merge branch 'master' of github.com:jjohnson42/confluent 2019-04-15 13:48:20 -04:00
Jarrod Johnson 0a816acf4f Add completion for attributes in node*attrib
Make some of the tedium of the long attribute names bearable
through tab completion.
2019-04-15 13:38:56 -04:00
Jarrod Johnson 2c9c778ca7 Fix compatibility without module_type
Some vendors do not provide module_type
value.  For such vendors, simply omit
the information rather than fail.
2019-04-15 11:08:56 -04:00
Jarrod Johnson bf005eace6 Merge branch 'master' of github.com:jjohnson42/confluent 2019-04-11 16:27:13 -04:00
Jarrod Johnson 34c6d6a4d7 Choose an easier name for the reader 2019-04-11 16:26:59 -04:00
Jarrod Johnson b402ddd656 Add more keystrokes and easier use
No longer require cbl file be specified manually.
2019-04-11 16:26:14 -04:00
Jarrod Johnson 1ae055fa8f Add '-n' option to nodeshell and noderun
Provide ability to suppress node prefix for nodeshell.
This for example can be a quick 'makehosts' substituted and
similar.
2019-04-11 09:18:37 -04:00
Jarrod Johnson 89cf255ae7 Add '-n' option to nodeshell and noderun
Provide ability to suppress node prefix for nodeshell.
This for example can be a quick 'makehosts' substituted and
similar.
2019-04-11 09:17:38 -04:00
Jarrod Johnson c070148aed Fix adequate check on inadequate IMMs 2019-04-08 10:15:27 -04:00
Jarrod Johnson d6097ca706 Merge branch 'master' of github.com:jjohnson42/confluent 2019-04-08 10:15:10 -04:00
Jarrod Johnson c3eed19309 Fix adequate check on inadequate IMMs 2019-04-08 10:14:49 -04:00
Jarrod Johnson 40dbe63336 Script to disable password expiry after expired on SMM 2019-04-05 16:53:32 -04:00
Jarrod Johnson d6ecee955b Skip empty nodes list
A noderange based nodegroup would have
the empty nodes list cluttering the output.
Skip empty nodes list in current settings.
2019-04-05 09:37:14 -04:00
Jarrod Johnson d7d3ae344c Skip empty nodes list
A noderange based nodegroup would have
the empty nodes list cluttering the output.
Skip empty nodes list in current settings.
2019-04-05 09:17:45 -04:00
Jarrod Johnson 5c4944a1e4 Provide a sample script for fixing expired credentials 2019-04-05 08:37:31 -04:00
Jarrod Johnson 06b31f4845 Add man page for collective command 2019-04-04 10:18:34 -04:00
Jarrod Johnson 7fecd0ac5c Add man page for collective command 2019-04-04 10:18:06 -04:00
Jarrod Johnson 36d5d60edc Autofill collective.manager on discovery if not set
To improve the ease of use, if an administrator has a collective but
does not designate a collective.manager for a node being discovered,
default to the collective member that executes the discovery.
2019-04-04 09:50:26 -04:00
Jarrod Johnson 6e26b19c67 Autofill collective.manager on discovery if not set
To improve the ease of use, if an administrator has a collective but
does not designate a collective.manager for a node being discovered,
default to the collective member that executes the discovery.
2019-04-04 09:49:02 -04:00
Jarrod Johnson 16430e1ec9 Enhance collective usage output
Collective usage output provided no hints as to how to access more detailed
help.  Amend the wording to make this more clear/obvious.
2019-04-03 14:23:50 -04:00
Jarrod Johnson 5d572f17f9 Enhance collective usage output
Collective usage output provided no hints as to how to access more detailed
help.  Amend the wording to make this more clear/obvious.
2019-04-03 14:23:30 -04:00
Jarrod Johnson ae49cf290e Fix key hold-down behavior in cbl reader
The retry print could get stuck in loop and the input
could queue up too many keypresses.
2019-04-03 11:44:05 -04:00
Jarrod Johnson 5ead803c8a Fix up the injected clear delimiting
The delimiting was not being navigated correctly.
2019-04-03 10:38:35 -04:00
Jarrod Johnson 7232a0c1b3 Fix SLP hangs on bad targets
Have SLP timeout if there are endpoints that can half-hang
a connection.
2019-04-03 08:35:25 -04:00
Jarrod Johnson dce25d802e Fix SLP hangs on bad targets
Have SLP timeout if there are endpoints that can half-hang
a connection.
2019-04-03 08:34:42 -04:00
Jarrod Johnson 835d1fc0ab Update the pyopenssl message
It makes it more clear that a restart would be
required to pull in updated dependency.
2019-04-02 09:40:18 -04:00
Jarrod Johnson c28a963d62 Update the pyopenssl message
It makes it more clear that a restart would be
required to pull in updated dependency.
2019-04-02 09:39:06 -04:00
Jarrod Johnson 9fd091daad Tolerate an XCC with downed web service
Make the best of the situation by trying to continue
without the policy applied.
2019-04-02 09:37:33 -04:00
Jarrod Johnson 3c21ca8739 Tolerate an XCC with downed web service
Make the best of the situation by trying to continue
without the policy applied.
2019-04-02 09:36:37 -04:00
Jarrod Johnson 996b1ba45b Rework prototype cbl reader
It is still not right, but it is a bit easier to work with
to figure out what's wrong with it.
2019-04-01 16:56:14 -04:00
Jarrod Johnson 4af1f998fb Fix nodeconfig formatting
The man page did not have a hard line break.
2019-04-01 14:19:18 -04:00
Jarrod Johnson b2c1137321 Fix nodeconfig formatting
The man page did not have a hard line break.
2019-04-01 14:16:20 -04:00
Jarrod Johnson d484e9db43 Update log.py comment for accuracy
The metadata format was out of date.
2019-04-01 10:35:20 -04:00
Jarrod Johnson 6397709e47 Try to force split across clear screens 2019-03-29 17:07:01 -04:00
Jarrod Johnson e0c0f0f1f3 Greatly flesh out the cbl reader 2019-03-29 16:01:40 -04:00
Jarrod Johnson ca29f6ae35 Opportunisticly start mac rescan on rescan
While the network rescan might be too slow to hold up
general rescan, at least begin a rescan of switches
when a rescan is requested.
2019-03-29 14:03:00 -04:00
Jarrod Johnson 2c8681a9f3 Opportunisticly start mac rescan on rescan
While the network rescan might be too slow to hold up
general rescan, at least begin a rescan of switches
when a rescan is requested.
2019-03-29 14:01:36 -04:00
Jarrod Johnson b927572872 Sample file for processing CBL files 2019-03-29 11:17:05 -04:00
Jarrod Johnson b5df380ee4 Update nodeinventory for redfish memory
Redfish doesn't present some data that was in the IPMI spec
2019-03-28 13:37:13 -04:00
Jarrod Johnson 5c61430ccc Add wildcard documentation to noderange man page. 2019-03-28 11:15:28 -04:00
Jarrod Johnson 2b275cd369 Add wildcard documentation to nodeattrib/nodelist 2019-03-28 11:15:23 -04:00
Jarrod Johnson f79dac7bd2 Add wildcard documentation to noderange man page. 2019-03-28 11:14:29 -04:00
Jarrod Johnson fc5f16fb01 Add wildcard documentation to nodeattrib/nodelist 2019-03-28 11:12:36 -04:00
Jarrod Johnson 907d25164f Replace the network error with a local error
This is a bit more clear about the cause when local commands fail.
2019-03-28 11:03:57 -04:00
Jarrod Johnson 0b85fab529 Replace the network error with a local error
This is a bit more clear about the cause when local commands fail.
2019-03-28 11:03:33 -04:00
Jarrod Johnson adb4ce919e Fix nodegrouplist man page 2019-03-28 10:14:30 -04:00
Jarrod Johnson 9379c85d0e Fix nodegrouplist man page 2019-03-28 10:05:35 -04:00
Jarrod Johnson 11ffa7a091 Fix debian build process 2019-03-26 13:50:15 -04:00
Jarrod Johnson bacba8972a Fix debian build process 2019-03-26 13:49:54 -04:00
Jarrod Johnson 5404497e70 Change wheezy to depend on old package name 2019-03-26 13:47:06 -04:00
Jarrod Johnson 70690517de Change wheezy to depend on old package name 2019-03-26 13:46:39 -04:00
Jarrod Johnson a3162daf62 Skip pushing static config if config already matches
A strategy of manually adding DHCP managed nodes produced static-baking
when not desired.  For now skip the baking in if the address matches.
2019-03-26 13:44:24 -04:00
Jarrod Johnson 8c886b751c Skip pushing static config if config already matches
A strategy of manually adding DHCP managed nodes produced static-baking
when not desired.  For now skip the baking in if the address matches.
2019-03-26 13:43:30 -04:00
Jarrod Johnson 69630edfa9 Handle more generics from Lenovo Redfish
Several generics can come back for adapters, handle them to trigger
pcie enhanced lookup.
2019-03-25 15:45:36 -04:00
Jarrod Johnson cdce1f1833 Add concurrency pool to pyghmi usage
pyghmi adds pool for concurrency, utilize that to accelerate operations
that need multiple resources in parallel.
2019-03-22 15:56:58 -04:00
Jarrod Johnson 48079f297b Change wheezy name to python-confluent... 2019-03-19 15:03:41 -04:00
Jarrod Johnson bf24d0f501 Change wheezy name to python-confluent... 2019-03-19 15:03:06 -04:00
Jarrod Johnson 1d6111b8dd Merge branch 'master' of github.com:jjohnson42/confluent 2019-03-18 15:02:55 -04:00
Jarrod Johnson e59d237d11 Add draft redfish plugin
It can do power, system config, set boot device, identify.
2019-03-18 15:02:21 -04:00
Jarrod Johnson fb3dc9a200 Merge branch 'master' of github.com:jjohnson42/confluent 2019-03-14 13:07:09 -04:00
Jarrod Johnson b0d2d44b75 Update to follow DCD
The DCD signal can be used to detect remote connect attempt
2019-03-14 13:05:59 -04:00
Jarrod Johnson f1e83d938b Add carrier detect to autocons sample
For genesis, not necessarily relevant, but may be
very relevant for OS behaviors in other contexts.
2019-03-14 09:33:58 -04:00
Jarrod Johnson e643b7ed7d Remove inadvertent duplicate command
Harmless, but should be removed.
2019-03-13 15:13:13 -04:00
Jarrod Johnson 163b29a07c Update autocons.py
This is the version that landed in xCAT genesis
2019-03-13 15:05:42 -04:00
Jarrod Johnson c5fa0bfd79 Draft attempt at autocons utility
Make every effort to do early boot console enablement.
2019-03-12 19:31:16 -04:00
Jarrod Johnson a258653186 Merge branch 'master' of github.com:jjohnson42/confluent 2019-03-11 09:08:28 -04:00
Jarrod Johnson 656e82c3fe Speed up cnos health and add stubs
Add concurrency to accelerate nodehealth and provide stubs
for the as-yet unimplemented functionality.
2019-03-08 16:05:42 -05:00
Jarrod Johnson 898ff065e0 Merge branch 'master' into umaster 2019-03-08 16:04:19 -05:00
Jarrod Johnson 779b5c9ede Speed up cnos health and add stubs
Add concurrency to accelerate nodehealth and provide stubs
for the as-yet unimplemented functionality.
2019-03-08 16:02:52 -05:00
Jarrod Johnson 5be08ddb1d Fix missing sub-health info on CNOS health 2019-03-08 13:38:38 -05:00
Jarrod Johnson 16abf7cb64 Fix missing sub-health info on CNOS health 2019-03-08 13:38:15 -05:00
Jarrod Johnson 269acf9943 Add CNOS plugin for Lenovo switches 2019-03-08 13:26:34 -05:00
Jarrod Johnson c649ae5fec Have it appropriate do IPv4 and IPv6 2019-03-08 13:25:53 -05:00
Jarrod Johnson c3f2e131b2 Add CNOS plugin for Lenovo switches 2019-03-08 13:25:04 -05:00
Jarrod Johnson 4124e0fcc0 Add ability for noderange to wildcard attrib names
Useful for net.*attribs to search when nic is unknown.
2019-03-07 15:20:19 -05:00
Jarrod Johnson 0e2e6267cd Add ability for noderange to wildcard attrib names
Useful for net.*attribs to search when nic is unknown.
2019-03-07 15:19:17 -05:00
Jarrod Johnson e8119d330d Add IPv4 search to copernicus search 2019-03-07 10:24:26 -05:00
Jarrod Johnson 5ae6717709 Rename findconfluent 2019-03-07 08:57:52 -05:00
Jarrod Johnson 0cd94447f7 Update to search multiple interfaces 2019-03-06 16:48:37 -05:00
Jarrod Johnson ce4f8c1837 Sample C code for locating confluent server 2019-03-06 16:22:55 -05:00
Jarrod Johnson 8ad06f79e7 Add nodersync
This provides bulk transfer with status to a noderange.
2019-03-04 14:28:52 -05:00
Jarrod Johnson 5481da269e Merge branch 'master' into 2.2 2019-03-01 15:27:40 -05:00
Jarrod Johnson 6ea307d415 Add nodegrouprename command 2019-03-01 15:27:13 -05:00
Jarrod Johnson 59aa23b2f5 Add noderename command 2019-03-01 15:21:29 -05:00
Jarrod Johnson 4446308030 Add ability to rename nodegroups
If we can rename nodes, should be able to rename groups.
2019-03-01 15:03:43 -05:00
Jarrod Johnson 7703c6c2ab Enable Server Portion of renamae node
This plumbs up through the messages and attributes plugin.
2019-03-01 14:37:07 -05:00
Jarrod Johnson f5b6d434f3 Fix node collection function signature 2019-03-01 13:37:13 -05:00
Jarrod Johnson 8ce5a7dccf Phase 1 of node rename support
Provide foundation for node renaming, including
updating groups and inheritance and notifying collection
watchers of the change, and updating the existing watchers
with the new notification fingerprint.
2019-03-01 13:21:57 -05:00
Jarrod Johnson 23c9e6315a Update node collection handlers for renamed
Rename support will provide a map of old to new name.  Have the
existing node collection watchers accept a new argument.
2019-02-28 15:57:35 -05:00
Jarrod Johnson 38f9583be3 Implement direct add switches
Allow addition of switches without associated
nodes.  This allows populating mac database
without requiring associated nodes.
2019-02-28 13:00:12 -05:00
Jarrod Johnson 1b355ec468 Merge branch 'master' of github.com:jjohnson42/confluent 2019-02-28 12:59:52 -05:00
Jarrod Johnson 2bbf4b9e98 Implement direct add switches
Allow addition of switches without associated
nodes.  This allows populating mac database
without requiring associated nodes.
2019-02-28 12:59:41 -05:00
Jarrod Johnson 1248894cf3 Amend nodegrouplist command 2019-02-28 10:24:24 -05:00
Jarrod Johnson c43365d2dd Merge pull request #102 from andywray/master
Add nodegrouplist

Will fix after merge
2019-02-28 10:17:43 -05:00
Jarrod Johnson 4e7c098e75 Remove stub of unimplemented set
nodestorage set for manipulating hotspare
and jbod was not done in time.
2019-02-28 09:16:25 -05:00
Jarrod Johnson ef6c89b883 Add man page for nodestorage 2019-02-28 09:13:57 -05:00
Jarrod Johnson 99c06813d9 Fix clearing validated attributes
Attributes that are validated can come
in as None to clear them, accept this
as valid as well.
2019-02-27 14:58:26 -05:00
Jarrod Johnson 686b59c2b4 Friendlier error on read during update
Firmware update error now sends a more friendly error to client.
2019-02-26 16:40:06 -05:00
Jarrod Johnson 46b909c291 Document nodesupport download behavior
nodesupport downloads locally, and does
not send through the client.  Document
the consequence of this for now.
2019-02-26 16:26:07 -05:00
Jarrod Johnson 9abb163c7e Merge branch 'master' of github.com:jjohnson42/confluent 2019-02-26 15:27:23 -05:00
Jarrod Johnson 7e25dd805f Update nodesupport usage
servicedata is, as yet, mandatory
2019-02-26 15:27:07 -05:00
Jarrod Johnson 31220292e5 Try debian build with minimum on breaks/replaces 2019-02-26 14:22:19 -05:00
Jarrod Johnson 161cf37f46 Fix nodediscover order and csv together 2019-02-26 13:57:11 -05:00
Jarrod Johnson ad64cda249 Rework transition package logic 2019-02-26 13:39:11 -05:00
Jarrod Johnson db812ac292 Specify confluent client deb name 2019-02-26 13:02:04 -05:00
Jarrod Johnson a322118877 Fix debian build 2019-02-26 11:09:23 -05:00
Jarrod Johnson ebfbbcca23 Fix reference to the logger class 2019-02-26 10:21:12 -05:00
Jarrod Johnson 275525d3f3 Add pointer to omitted option 'all'
Nodegroupattrib can take all as an argument, show it in usage.
2019-02-25 15:27:55 -05:00
Jarrod Johnson 938a6e44df Add checking for noderange conflict
A confluent nodegroup may either be a normal static one, which
can be used for attribute inheritence, or a dynamic one, which
cannot be used with static list or static attributes.

Warn the user when they try to set that up to make it more obvious
that the dynamic groups can't do what they are trying to do.
2019-02-25 15:23:54 -05:00
Jarrod Johnson ca6b203a09 Format console message
The message wrapping was very hard to read
on a default 80 wide terminal.
2019-02-22 10:28:10 -05:00
Jarrod Johnson c478cb5d6e Update language of empty buffer warning
It was confusing and failing to point out
possibilities that were frequently occuring.
2019-02-22 09:36:25 -05:00
Jarrod Johnson ca9e7d1d93 Attempt to catch filesystem full condition
Provide a more obvious behavior when filesystem fills
to explain confluent behavior in this situation.
2019-02-21 16:32:37 -05:00
Jarrod Johnson 2691722f48 Update usage example in nodeconfig
Provide a customized usage string to more accurately show
the structure of a nodeconfig command.
2019-02-21 15:02:53 -05:00
Jarrod Johnson e194222553 Capture trace data on firmware update failure
In the event of an unanticipated firmware exception,
capture to log.  Expected error conditions should have specific handlers
to avoid the tracellog treatment.
2019-02-21 14:40:32 -05:00
Jarrod Johnson 8f611f0e59 Advertise package name change
Have the new package name change in
control to prompt upgrade to do
the right thing.
2019-02-21 10:46:53 -05:00
Jarrod Johnson 1fdcf19563 Merge branch 'master' of github.com:jjohnson42/confluent 2019-02-20 14:13:59 -05:00
Jarrod Johnson add1a1b32a Add noderange to nodegroupattrib
noderange attribute on group was not
being presented by nodegroupattrib all.
Add the attribute explicitly to handle the
discrepency.
2019-02-20 14:13:48 -05:00
Jarrod Johnson 8abe384e1a Add notation in expression page about shell conflict 2019-02-14 19:29:56 -05:00
Andy Wray 14577be963 Add nodegrouplist 2019-02-13 15:36:59 -05:00
Jarrod Johnson e6b8d0dabc Make dependencies adaptable for wheezy and non-wheezy
Newer distributions we have different requirements.
2019-02-12 16:18:21 -05:00
Jarrod Johnson b1a91ad409 Add support for comments in batch
Use # as comment character in nodeconfig batch.
2019-02-12 11:12:47 -05:00
Jarrod Johnson 996fd82920 Provide quality output on list output/default
The output is better than default python treatment of list formatting.
2019-02-12 10:09:07 -05:00
Jarrod Johnson 5e6c66826f Provide error on useless api call
If making an api call against a noderange to set attributes and that noderange is blank,
provide feedback to help user know that
nothing happened.
2019-02-11 16:01:30 -05:00
Jarrod Johnson 22d79867c8 Reorganize builddeb to preserve git tree
The git tree is needed to successfully build
2019-02-11 15:16:24 -05:00
Jarrod Johnson 52d25d563b Correct spelling error in builddeb 2019-02-11 14:47:22 -05:00
Jarrod Johnson 68eeb95ea3 Ensure directory exists prior to use 2019-02-11 14:06:28 -05:00
Jarrod Johnson 95d5ff6a4c Have builddeb move to tmp
This avoids build trampling current working tree.
2019-02-11 13:54:49 -05:00
Jarrod Johnson b42114bea0 Actually apply sed to file
The sed was failing to save result to disk.
2019-02-11 11:20:59 -05:00
Jarrod Johnson e0877bc0b1 Workaround older python lack of SSLEOFError
Older python does nat have this exception,
stub it out for older python.
2019-02-11 11:00:47 -05:00
Jarrod Johnson 5289d34206 Fix the exception name in previous commit. 2019-02-11 10:56:29 -05:00
Jarrod Johnson f7f8247d02 Fallback to older eventlet signature
Older eventlet did not understand some arguments
on wsgi.  Fallback to not using those arguments
if unavailable.
2019-02-11 10:51:56 -05:00
Jarrod Johnson 57e23a6f52 Add missing dependencies to debian builds
Debian builds currently lean on py2dsc, but misses a few key
dependencies.
2019-02-11 10:09:02 -05:00
Jarrod Johnson 73b234d29e Support pre-cryptography paramiko
Older paraiko may be in use that does
not have cryptography requirement, address
error by only conditionally checking cryptography.
2019-02-08 14:43:20 -05:00
Jarrod Johnson bfe55e276d Do not try to delete a node if not in discovery data
This could produce undesired traces
2019-02-06 09:19:55 -05:00
Jarrod Johnson 44bcca99b6 Delete discovery entries related to deleted node
On node deletion, it can be confusing if a stale discovery
entry persists.  Delete such entries upon deletion.
2019-02-06 09:15:51 -05:00
Jarrod Johnson 4cb595684e Fix debian package name 2019-02-05 15:41:03 -05:00
Jarrod Johnson b153a14ff3 Have builddeb build names consistent with RPM
The deb package names and locations were inconsistent
with the RPM based distributions.  Correct this behavior.
2019-02-05 13:12:35 -05:00
Jarrod Johnson b620838189 Move rather than copy output debs 2019-02-05 10:36:06 -05:00
Jarrod Johnson 4540354ff2 Add optional location for builddeb 2019-02-05 10:29:31 -05:00
Jarrod Johnson 74963a73cc Do cd in the correct location 2019-02-04 15:35:55 -05:00
Jarrod Johnson 9fe200b525 Make sure builddeb goes to top of dir before cleanup 2019-02-04 15:16:52 -05:00
Jarrod Johnson b07d4e9736 Clean up extracted directory on successful deb 2019-02-04 15:14:39 -05:00
Jarrod Johnson f649efa110 Add script to build .deb
Easier support for debian and ubuntu.
2019-02-04 15:05:36 -05:00
Jarrod Johnson 521013e50a Implement SMM password policy configuration
The discovery.passwordrules is extended to support the SMM
2019-02-01 15:42:44 -05:00
Jarrod Johnson 25c8f93336 Provide error in the console when console.method is unset
This was a common mistake, and warranted a more blatantly obvious
output.
2019-02-01 12:49:00 -05:00
Jarrod Johnson 59f00dd10b Set password before access
In at least one scenario, we want the password to be squared away
before we start manipulating the user access level.
2019-01-31 15:59:49 -05:00
Jarrod Johnson 2e93af9b5e Treat '' same as None for plugin specification
From the CLI, both are the same and there is no good reason to
treat them differently from each other.
2019-01-31 15:58:57 -05:00
Jarrod Johnson 337ab3b1a0 Merge branch 'master' of github.com:jjohnson42/confluent 2019-01-31 14:29:03 -05:00
Jarrod Johnson f4cf74b699 Fix modifying dictionary while iterating
Need to make a stable value before iterating to delete.
2019-01-31 14:28:45 -05:00
Jarrod Johnson 085981f74c Remove spurious debug output
Some spurious debug output was injected by mistake.
2019-01-30 13:45:38 -05:00
Jarrod Johnson 8a5f1c6dc5 Parse string arguments to nodes attributes as noderange
Since everywhere else accepts noderange, it is a reasonable
expectation for the nodes attribute on nodegroups to also
take a noderange.  Correct this inconsistency.
2019-01-30 09:55:38 -05:00
Jarrod Johnson 09cb6963f0 Add attempt to present status data
tcons made this attempt, also implement this on a best
effort basis.
2019-01-29 16:02:54 -05:00
Jarrod Johnson 188feec0b4 Repair if transactioncount exists but wrong size
If filesystem is full, this could be a side effect that would impact
a later start in a collective context.  This mechanism induces repair
from a collective peer.
2019-01-28 16:20:07 -05:00
Jarrod Johnson 1902a333ae Rework audit on unix socket
Capture root in audit and be consistent about audit skipping between
socket and http.
2019-01-28 15:03:45 -05:00
Jarrod Johnson f6c46ddcb8 Fix checking causing breaking of setting empty
Empty values are always valid, do not check those.
2019-01-24 11:18:40 -05:00
Jarrod Johnson e23253815c Fix checking code with custom fields
For custom fields, do not cause an unexpected errors.  The lookup on the
validattrs will now default to nothing found instead of error.
2019-01-24 11:07:58 -05:00
Jarrod Johnson d979d29b0b Implement checking of input attributes
For attributes that have a well known set of inputs, provide the
data to the messages layer to provide a useful error to the user.
2019-01-22 16:02:17 -05:00
Jarrod Johnson bca676ed15 Have nodestorage assume show if no args
Confluent commands with noderange generally
assume 'get' if no option.  Be compliant with
this consistency.
2019-01-18 14:54:55 -05:00
Jarrod Johnson deed8b4b9b Fix behavior of SSDP discovery handler
It was not tracking multiple peers and it was also
extending the scan longer than needed.
2019-01-17 11:37:09 -05:00
Jarrod Johnson 2c94a10e23 Add complexity and reuse rules to discovery
Discovery can now relax two more common policies that have users
wanting to disable them.
2019-01-17 11:02:05 -05:00
Jarrod Johnson 299181223e Add '-b' option for collate
It was requested to be able to designate the 'reference' node for '-d'
comparison.  This implements that request.
2019-01-17 10:35:22 -05:00
Jarrod Johnson 1378b01feb Fix nodeconfig handling of multiple colons
If multiple colon designated settings are present, correctly
associate settings with appropriate key.
2019-01-16 11:31:06 -05:00
Jarrod Johnson 18713797b9 Fix pane splitting with large numbers of nodes
The tcons logic to select layout each iteration is now replicated,
to avoid too-small panes.
2019-01-15 16:00:39 -05:00
Jarrod Johnson 6588ac0990 Fix nodereseat false success message
nodereseat was reporting success when failure was actually encountered.
Address by injecting the node specific error.
2019-01-15 11:51:43 -05:00
Jarrod Johnson efd13c960f Add man page for nodelicense 2019-01-14 16:08:48 -05:00
Jarrod Johnson 4f14de0136 Add nodelicense command
This provides Bash access to the license management features.
2019-01-14 16:06:13 -05:00
Jarrod Johnson 6cecf6635c Merge branch 'master' of github.com:jjohnson42/confluent 2019-01-14 16:02:37 -05:00
Jarrod Johnson 5b7807fe3d Add nodelicense command
This provides Bash access to the license management features.
2019-01-14 16:02:15 -05:00
Jarrod Johnson 5f52ee9578 Support unique filenames through attribute expansion
Particularly in trying to support license keys, serial number
handling is at least required.  Easiest and most powerful and most
consistent path is to use the same attribute expansion as everywhere
else.
2019-01-14 15:41:17 -05:00
Jarrod Johnson 88275f5110 Fix xcc handler on incomplete data
A datum may come in without an attribute member at all.  Treat
this as 'false', obviously.
2019-01-14 15:37:04 -05:00
Jarrod Johnson 55c5bff0f9 Add support for uploading a license key
This provides the most important piece of license key management.
2019-01-14 15:09:49 -05:00
Jarrod Johnson 3782127590 Implement retrieving license info from XCC
With changes to pyghmi, we can now get license data.  Prepare
API to provide information about the licenses.
2019-01-14 14:55:03 -05:00
Jarrod Johnson 74355bd6f9 Merge branch 'master' of github.com:jjohnson42/confluent 2019-01-14 09:17:01 -05:00
Jarrod Johnson bc995520b7 Fix confetty tab completion
The refactoring to main caused scope of readline to be incorrect.
Pulled the scope back into namespace.
2019-01-14 09:15:59 -05:00
Jarrod Johnson 2d63e68494 Enable support for TCP_FASTOPEN
It might matter in some context.
2019-01-10 15:17:34 -05:00
Jarrod Johnson ceba3ff34f Fix remote collective console
The changes to support size data for ssh was not carried fully
through the proxyconsole object.  Fix this support.
2019-01-09 11:34:19 -05:00
Jarrod Johnson 334ec00f3f Properly report client use of bad async id
Previously an expired async id would trigger unexpected error.  Now
it returns an error to client and avoids the trace log.
2019-01-08 15:43:41 -05:00
Jarrod Johnson 35380e5bac Add missing import
errno was erroneously omitted from previous commit
2018-12-12 10:15:33 -05:00
Jarrod Johnson 66471624bc Support caching SDR for pyghmi versions that support it
This will improve performance across confluent restarts for commands
like nodesensors, nodehealth, and nodeinventory.
2018-12-12 09:53:36 -05:00
Jarrod Johnson 8a03bc48de Tentatively store certutil
Commit to repository, even though not yet used.  It is likely to
be renamed.  The purpose is to help generate an appropriate self signed
cert for https including all the ip addresses as subject alternative
names so that names or addresses may be used with installers that
have had the cert injected.
2018-12-11 13:51:46 -05:00
Jarrod Johnson 5831be091a Auto-rescan on missed manual discovery
Same as in the bulk discover support.
2018-12-11 09:17:08 -05:00
Jarrod Johnson 8197c750bb Add batch support to nodeconfig 2018-12-10 09:47:49 -05:00
Jarrod Johnson a9f0312acd Add support for exporting only a mac csv
For the specific task of importing mac addresses but not
perturbing other xCAT configuration, provide output that can be used
with tabrestore.
2018-12-07 14:08:34 -05:00
Jarrod Johnson 21700e914a Add confluent2xcat command
Add command for exporting node data as an xCAT stanza file
2018-12-07 13:59:01 -05:00
Jarrod Johnson 5e15ae8e30 Update nodeconsole man page for tiling 2018-12-04 10:41:50 -05:00
Jarrod Johnson 5eef76f5d3 Try a more straightforward use of tiling
Rather than retiling for every node, defer tiling until the end.
This dramatically improves the startup time for a moderate number
of systems.
2018-12-03 15:57:08 -05:00
Jarrod Johnson df72bfa715 Merge branch 'master' of github.com:jjohnson42/confluent 2018-12-03 15:53:24 -05:00
Jarrod Johnson a6333459c4 Add tile option to nodeconsole
This allows nodeconsole to produce the 'tcons' behavior without
xCAT dependency.
2018-12-03 15:53:15 -05:00
Jarrod Johnson 075891bf74 Add option to confetty to help keep terminal open on early exit
For contexts where early execution execution almost always means
some sort of error, but the terminal automatically closes,
provide a minimum time to execute option so confetty will
forcibly slow things down in such a likely scenario.
2018-12-03 14:49:19 -05:00
Jarrod Johnson a937511354 Fix nodesetboot argument parsing 2018-11-30 16:09:47 -05:00
Jarrod Johnson 22503e7e11 Inject blank lines in ansible hosts file
This looks a bit more like a natural hosts file.
2018-11-29 15:21:37 -05:00
Jarrod Johnson 01d73308ed Pull back spurious blank lines 2018-11-29 15:04:56 -05:00
Jarrod Johnson cb16385ddb Add confluent2ansible
Utility that wil take a noderange and generate an appropriate
ansible hosts file, including carrying over pertinent groups.
2018-11-29 15:04:27 -05:00
Jarrod Johnson 853585f942 Have nodeattrib output be sorted
This make nodelist and nodeattrib output much easier to follow.
2018-11-29 10:02:37 -05:00
Jarrod Johnson 53996a1e9d Increase likelihood of IPMI results being sorted
Without incurring execution penalty, endeavor to sort available
data in an opportunistic way.  This certainly does not guarantee order,
but it makes it highly likely for quick queries to appear orderly.
2018-11-29 09:43:53 -05:00
Jarrod Johnson 558171124c Fix nodediscover man page formatting 2018-11-27 16:14:15 -05:00
Jarrod Johnson 05d028e9e9 Fix formatting of nodediscover man page
The list of fields could not be nicely nested in ronn,
give up but have hard line breaks as a compromise.
2018-11-27 15:36:36 -05:00
Jarrod Johnson b0a3bb18fe Add terminal resizing to http api
This enables javascript clients to submit terminal changes.
2018-11-27 09:12:53 -05:00
Jarrod Johnson 7207013abc Have httpapi be able to accept initial size on consoles
consoles and shells can now put into the body parameters including
the width and height.
2018-11-26 16:43:52 -05:00
Jarrod Johnson b511a02f20 Have correct size on connect for shell session
In addition to resize, also support initial size being set
2018-11-26 16:21:31 -05:00
Jarrod Johnson c60cb3a027 Implement resize from CLI client
The CLI resize is wired up for ssh usage.  At the time of this commit,
initial size is not handled.
2018-11-26 15:31:36 -05:00
Jarrod Johnson 9f137fa6d4 Begin work to send resize to remote
While we do not have a path forward for auto-resize in SOL consoles,
we can provide the capability for ssh consoles.  The CLI being enabled
is of course nice, but running ssh directly is still suggested in such
a context and ultimately the Web GUI will be the most prominent
consumer of the information.
2018-11-20 16:01:36 -05:00
Jarrod Johnson aa92898249 Have nodestorage create optionally read names from args
This is more symmetric with the way delete works.
2018-11-19 15:32:03 -05:00
Jarrod Johnson 2cb694b374 Update man page of nodediscover 2018-11-19 14:46:47 -05:00
Jarrod Johnson 24fd1e72e1 Expose advertised, not valided IPs in discovery
There are circumstances where the advertised IP is
useful information, even if that IP cannot be validated.

It is also the case that most of the time the advertised IP
is useless if not validated, so it is an optional field.
2018-11-19 14:39:56 -05:00
Jarrod Johnson 5b21a5b2c3 Fix spreadsheet support for the BMC gateway
The spreadsheet support used incorrect attribute name.
2018-11-19 09:50:04 -05:00
Jarrod Johnson c8ad94313e Provide useful error when trying to delete a volume that doesn't exist 2018-11-16 11:54:46 -05:00
Jarrod Johnson f71d51769b Provide clean print of created volumes
This presents the returned data in a manner consistent
with nodestorage show
2018-11-16 11:27:26 -05:00
Jarrod Johnson b5213e6972 Show data about created volumes upon volume completion.
Provide concrete feedback as to the results of the creation operation.
2018-11-16 11:19:27 -05:00
Jarrod Johnson 8d72f4d64a Fix volume name creation
The volume names were not being honored, fix passing the arguments.
2018-11-15 16:25:03 -05:00
Jarrod Johnson fcd46c7b72 Provide cleaner output for nodestorage delete
Rather than dumping the structure of the response, provide a consistent
plain text output.
2018-11-15 16:18:24 -05:00
Jarrod Johnson 6efb8ff1b7 Add globbing protection to nodestorage command
This mitigates risk of mistargeting a nodestorage command due to
unfortunate bash glob match.
2018-11-15 16:15:01 -05:00
Jarrod Johnson c227c2353b Provide better error on nodediscover assign
Rather than the rather useless unexpected error, actually
describe specific cases of wrong username or password.
2018-11-15 13:41:48 -05:00
Jarrod Johnson cb2133e871 Change strategy for pam.d entry
The symbolic link did not stay a symbolic link.
Use %post in rpm instead.
2018-11-14 15:11:05 -05:00
Jarrod Johnson c56c1948a9 Add confluent pam to packaging
This enables PAM passwords to work by default,
rather than requiring user to perform more
configuration.
2018-11-14 14:55:26 -05:00
Jarrod Johnson 34b30a1bd8 Rework password authentication
Have the cache operate against the sha256 of the passphrase rather
than the passhprase directly (mitigate presence of password in memory).

Pull PAM into the cache behavior to accelerate PAM based usage.

Rearrange things so that both built in confluent and PAM passwords
can be used.
2018-11-14 14:52:03 -05:00
Jarrod Johnson 0e7bfe4f40 Use scanning status to control rescan behaviors
This causes the manual assignment to sleep less and for the rescan
command to meaningfully slow down shell requests to provide better sense
of when scan completes.
2018-11-14 14:17:12 -05:00
Jarrod Johnson a93a759b72 Make /networking/macs data available through nodediscover
This will help combine the data together more easily.
2018-11-14 14:11:03 -05:00
Jarrod Johnson cba514692c Bring the XCC check fix to IMM
The same check that works for XCC should be applied to IMM
2018-11-13 16:17:39 -05:00
Jarrod Johnson 2bb0a70a24 Provide an xid if one is not preserved
The SLP xid may be gone in a recheck, default to 42 in such a case.
2018-11-13 16:09:37 -05:00
Jarrod Johnson 1b84cd43fe Fix the auto-healing support
There was a mismatch between the handlers signature and expectation by
the discovery core.
2018-11-13 15:55:24 -05:00
Jarrod Johnson 39839a9f2a Add auto-healing of incomplete SLP data
If a device handler indicates an SLP data as incomplete, schedule a
attempt to fixup that specific one.
2018-11-13 15:37:41 -05:00
Jarrod Johnson 8473cb1cf8 Survive SLP values with =
If the slp parsing produces = in the value, ignore it and do best
effort to get the viable data out.
2018-11-09 15:38:40 -05:00
Jarrod Johnson 1a10b5d747 Have confluent2lxca use current config for address
In a confluent2lxca scenario, it is highly probably that confluent's
attribute is not viable for LXCA usage (link local address).

Give the current configuration the opportunity to replace the
confluent reference with the current IPv4 of the host.
2018-11-09 08:53:10 -05:00
Jarrod Johnson 82a7012527 Add a command to generate xClarity Administrator import
This will generate a bulk import file for use with the xClarity
Administrator appliance.
2018-11-09 08:33:04 -05:00
Jarrod Johnson 199a948b33 Fix ordering without custom field list
The order argument only had any meaning when used in custom fields.
Add a loop in the custom field agnostic section of the code to correct
the behavior.
2018-11-09 08:32:28 -05:00
Jarrod Johnson 6fc305d6a0 Add ordering by field to nodediscover list
This provides single column sort for more managable listing of
nodediscover items.
2018-11-06 16:34:22 -05:00
Jarrod Johnson 75b6221dcf Allow requesting custom columns
This allows for more data to be presented and some useless data
to be pruned.
2018-11-06 15:52:20 -05:00
Jarrod Johnson bd5032268e Fix csv output mode of nodediscover list
The changes made for tabulator broke csv output, fix that back.
2018-11-06 15:33:04 -05:00
Jarrod Johnson 0aa0289b06 Clean out dead code
There is some dead and commented code to be cleaned out.
2018-11-06 15:30:18 -05:00
Jarrod Johnson 96df7871cb Convert nodediscover to use Tabulator
This opens the path to more customized output and have the appropriate
formatting.  Also revise the Tabulator code to more closely match
the original nodediscover list output.
2018-11-06 15:28:27 -05:00
Jarrod Johnson 82ee69b56c Age out macmap on node database changes
This mitigates problems where automation fails to quickly
react to changes/fixes in the net attributes.
2018-11-06 08:18:37 -05:00
Jarrod Johnson 37899ad39e Do not inject 'None' into builddesc
If such data came from remote system, do not cause a stack trace.
2018-11-05 10:43:36 -05:00
Jarrod Johnson 43fb4467b7 Add error if chain goes off into non-existent nodes
Various behaviors break if the administrator declares a chain goes
to nowhere, throw up an error and hopefully see it caught to detect
this misconfiguration.
2018-11-02 15:56:34 -04:00
Jarrod Johnson f5f9403eff Fix incorrect variable name in nodediscover
The field name was incorrectly referencing the last field
rather than the current.
2018-11-02 15:50:02 -04:00
Jarrod Johnson 78e7381442 Add implicit rescan on unmatched csv data
If the csv file contains criteria we can't match, implicitly
try a rescan automatically to try to catch it.
2018-11-02 15:18:55 -04:00
Jarrod Johnson 005cefc914 Add volume creation and deletion to nodestorage 2018-11-01 16:59:42 -04:00
Jarrod Johnson 5f38cce51e Finish nodestorage show
It now shows the pertinent details of storage on a node.
2018-11-01 10:43:28 -04:00
Jarrod Johnson 5d358eaeb0 Switch to output format more like nodeinventory
Other confluent commands (apart from nodediscovery) set a particular
convention.  Go by that convention (at least by default)
2018-10-30 16:28:59 -04:00
Jarrod Johnson 115a6e9dd1 Add helper for tabular output
Provide a quick class for auto-sizing tabular output.
2018-10-30 16:23:34 -04:00
Jarrod Johnson a034d05cc8 Draft tabular output of disk information
Print out roughly the data that will be desired in the storage show.
2018-10-30 16:10:37 -04:00
Jarrod Johnson 6378f823f3 Add resource name to the storage messages
The storage messages need to have the references preserved
for CLI consumption.
2018-10-30 15:46:54 -04:00
Jarrod Johnson 079dfed11e Add type to storage messages
Storage messages are likely to be grouped together and need
more to disambiguate them.
2018-10-30 15:28:03 -04:00
Jarrod Johnson d836bf7298 Map health to nagios compatible codes
nodehealth adds granularity and health to the return code.
Rather than only reflecting failure running command, also
have nodehealth assign exit code according to the expectations
of nagios.
2018-10-30 13:56:09 -04:00
Jarrod Johnson 25b969a4db Add 'all' element to configuration/storage
Provide a way to provide all the data about storage configuration
in a single call.
2018-10-30 10:25:41 -04:00
Jarrod Johnson 950abde20e Remove 'all' from storage subelements
This concept is potentially problematic, as a volume named 'all' is
valid.  Additionally, if really wanting all, the caller probably wants
the full storage configuration, better provide that up a level to
avoid requiring many calls to do that.
2018-10-30 10:05:48 -04:00
Jarrod Johnson 37be696f23 Merge branch 'master' of github.com:jjohnson42/confluent 2018-10-29 14:56:37 -04:00
Jarrod Johnson 73cab3774d Add support for volume creation 2018-10-26 14:31:28 -04:00
Jarrod Johnson c2d52d4f83 Add functionality to delete volumes
Further enriching the storage api by adding the ability to delete
a volume.
2018-10-25 16:31:32 -04:00
Jarrod Johnson fd17559a73 Show result of changing disk state.
When changing disk state, let the caller know the result explicitly.
2018-10-25 16:11:54 -04:00
Jarrod Johnson 3945ccd5c3 Advance storage configuration API
Enumerate disks in an array in disks list.

Add array associated with disk to disk data.

Show detailed data on arrays and volumes.
2018-10-25 16:07:07 -04:00
Jarrod Johnson 7c006b33bc Implement functionality to configure disks
Disks may now have their state changed between hotspare, unconfigured,
and jbod.
2018-10-25 14:48:58 -04:00
Jarrod Johnson c96b5f0270 Fix spurious trace on immediate exit confetty
When confetty exits without doing anything, it causes
sockapi to reference an empty request.  Check for that before
checking if it is a collective request.
2018-10-25 14:12:15 -04:00
Jarrod Johnson 7e72240d98 Add protection against duplicate import data
nodediscover assign from csv will now guard against human error
in putting the same data in multiple times.
2018-10-25 11:07:56 -04:00
Jarrod Johnson c150a848fd Normalize drive state
Since drive state is a key api feature, make sure it is normalized
to a set vocabulary.
2018-10-24 16:36:38 -04:00
Jarrod Johnson bc7a197a8c Merge branch '18csi' 2018-10-23 09:16:32 -04:00
Jarrod Johnson 6973736c6a Set password before setting username
The BMC is at much higher risk for rejecting the password
(e.g. the password does not pass complexity requirements).  If the
username changes, but the password is still default, it is very
confusing.  Give the password change the chance to break the
configuration process first.
2018-10-23 09:16:15 -04:00
Jarrod Johnson d70c3dcad7 Merge branch '18csi' 2018-10-23 09:10:01 -04:00
Jarrod Johnson f9055a258e Provide specific completion behavior for noderun/nodeshell 2018-10-23 09:09:03 -04:00
Jarrod Johnson 76a96fa054 Merge branch '18csi' 2018-10-18 15:44:13 -04:00
Jarrod Johnson c784a4ec9b Fix noderun output of continuing commands
noderun always only output at the end, fix the
mistake in the select statement.
2018-10-18 15:43:57 -04:00
Jarrod Johnson 2ff47b886d Fix noderun output of continuing commands
noderun always only output at the end, fix the
mistake in the select statement.
2018-10-18 15:43:36 -04:00
Jarrod Johnson 51037e2487 Change 'status' to 'state'
This is more consistent with several other confluent messages.
2018-10-17 09:37:31 -04:00
Jarrod Johnson 614b916ca4 Merge branch '18csi' 2018-10-16 09:39:00 -04:00
Jarrod Johnson 4077346d30 Rename api from drives to disks
It just feels more appropriate to use disks rather than drives.
2018-10-16 09:00:25 -04:00
Jarrod Johnson 8a4bf22a7e Further flesh out the storage api support
Add ability to list arrays and volumes.
2018-10-15 16:27:12 -04:00
Jarrod Johnson f2dd501de9 Do not proceed to try to upload if file doesn't exist
After relaying the error, it went ahead and
attempted the update, contrary to any reasonable expectation.
2018-10-15 11:15:13 -04:00
Jarrod Johnson f835057ae4 Do not proceed to try to upload if file doesn't exist
After relaying the error, it went ahead and
attempted the update, contrary to any reasonable expectation.
2018-10-15 11:14:59 -04:00
Jarrod Johnson 48913218a6 Merge branch 'master' of github.com:jjohnson42/confluent 2018-10-12 15:47:44 -04:00
Jarrod Johnson a11e4b71ce Merge branch '18csi' 2018-10-12 15:47:38 -04:00
Jarrod Johnson e9ba49a4aa Intercept another 'unexpected error'
During a particularly hectic init, Invalid Session ID
may occur if a command is ran particularly early.  Intercept
and replace a more clean message.
2018-10-12 15:46:54 -04:00
Jarrod Johnson deb90fbca9 Fix trace on early console connect
If the trace happens before tracelog is ready, just print the output
to the stdout log for now.
2018-10-12 14:56:54 -04:00
Jarrod Johnson 8cae5ea101 Fix trace on early console connect
If the trace happens before tracelog is ready, just print the output
to the stdout log for now.
2018-10-12 14:28:21 -04:00
Jarrod Johnson f830658818 Merge branch '18csi' 2018-10-12 13:33:09 -04:00
Jarrod Johnson 3105b9b1f9 Significantly rework the collective startup behavior
One, make the tracking bools enforce a lock to reduce confusion

Treat an initializing peer as failed, to avoid getting too fixated
on an uncertain target.

Make sure that no more than one follower is tried at a time by
killing before starting a new one, and syncing up the configmanager
state

Decline to act on an assimilation request if we are trying to connect
and also if the current leader asks us to connect and we already are.

Avoid calling get_leader while connecting, as that can cause a member
to decide to become a leader while trying to connect, by swapping
the reactions to the connect request.

Avoid trying to assimilate existing followers.

Fix some logging.
2018-10-12 11:45:23 -04:00
Jarrod Johnson f525c25ba6 Provide more verbose collective logging
This helps understand the flow in practice of collective behavior.
2018-10-11 15:15:11 -04:00
Jarrod Johnson 3012de1fe4 Prioritize deletion of transactioncount
If the invalidation is incomplete, make sure that transactioncount
is invalidated first to avoid it being able to propogate through
a collective.
2018-10-11 09:16:57 -04:00
Jarrod Johnson be930fc076 Add missing subsystem marker from a collective log 2018-10-10 16:30:28 -04:00
Jarrod Johnson 2d0199a4e9 Wrap bdb deletion in same lock that sync itself uses
If os.remove happens at a bad time, it causes an unfortunate behavior
in dbm.  Serialize this sort of operation to avoid the bad behavior.
2018-10-10 15:24:55 -04:00
Jarrod Johnson 6b70a4322a Fix rollback
The fix for the stale data introduced breaking clear rollback
Restore the behavior and make self._cfgstore a somewhat slower property
for now.
2018-10-10 15:22:20 -04:00
Jarrod Johnson 6a784e3a1c Ensure sync is complete prior to leaving configmanager sync
The initialization lock is meant to avoid collective and generic
initialization stepping on each other.  This is somewhat reduced in
efficacy if one has a sync running while the other is changing relevant
data.
2018-10-10 14:49:33 -04:00
Jarrod Johnson 3b2b96a4cf Force fullsync if dead sync thread likely
If the sync thread died previously, force the next sync to be full.
2018-10-10 14:32:13 -04:00
Jarrod Johnson 32ddb33de3 Fix error when trying to do fullsync without globals yet
If globals is missing, then do not break the sync trying to handle it
2018-10-10 13:11:15 -04:00
Jarrod Johnson b77ed8dbff Fix config sync on dead writer
The sync thread can die without clearing syncrunning.  Make sure that
the thread is alive *and* that the thread has not indicated
intent to give up.
2018-10-10 13:07:27 -04:00
Jarrod Johnson d5c093a30d Provide fallback for unexpected reply in collective show 2018-10-10 09:46:01 -04:00
Jarrod Johnson cf9d2a43e8 Revert "Provide fallback for unexpected reply in collective show"
This reverts commit 2f566fb81d.
2018-10-10 09:44:06 -04:00
Jarrod Johnson 2f566fb81d Provide fallback for unexpected reply in collective show 2018-10-10 09:41:25 -04:00
Jarrod Johnson 92a3b02dc5 Merge branch '18csi' 2018-10-10 09:24:18 -04:00
Jarrod Johnson 94c8cf3ff2 Apply the correct change to collate 2018-10-10 09:24:01 -04:00
Jarrod Johnson 049897fc15 Merge branch '18csi' 2018-10-10 09:21:07 -04:00
Jarrod Johnson 8741a27c24 Merge branch '18csi' of github.com:jjohnson42/confluent into 18csi 2018-10-10 09:09:12 -04:00
Jarrod Johnson 1c494fc4fc Correct mistake in the collate log support 2018-10-10 09:08:45 -04:00
Jarrod Johnson 1ee418392f Provide better error behavior on missing collective.manager
collective.manager was a blanket response, make it per node and
only triggered by the bad nodes, not the rest.
2018-10-09 15:44:17 -04:00
Jarrod Johnson 2a7eeb6e08 Fix missing argument in calling a function 2018-10-09 15:21:11 -04:00
Jarrod Johnson 5c83c78a90 Add warning on incompatible ssh key with SLES12 2018-10-09 14:44:06 -04:00
Jarrod Johnson 6a466b0100 Avoid proxy consoles generating proxy consoles
When the client is a proxy term, disable ability to produce
proxy terminals.  This was wreaking havoc with client
count with ghosts and triggering output multiplication.
2018-10-09 13:21:02 -04:00
Jarrod Johnson 67b93ee6b0 Merge branch '18csi' 2018-10-09 09:46:44 -04:00
Jarrod Johnson 6b4a21d613 Add log option to collate for per node logs 2018-10-09 09:37:25 -04:00
Jarrod Johnson 5f46899358 Prevent clear_configuration from invaliding existing ConfigManager
Clear out the existing dictionary instead of replacing it.

This prevents configmanager objects from being stuck.
2018-10-08 16:51:58 -04:00
Jarrod Johnson a3bc17b465 Merge branch '18csi' 2018-10-08 15:56:43 -04:00
Jarrod Johnson 20a37f8db5 Add mention of the -u options to the manpages. 2018-10-08 11:02:33 -04:00
Jarrod Johnson c6b8aaf372 Fix mistake in the nodegroupdefine man page 2018-10-08 10:57:48 -04:00
Jarrod Johnson 5baab5bef4 Add more stateful to completion
Allow it to sense words already used in command.  Refactor to common
code for similar ones.
2018-10-08 10:47:38 -04:00
Jarrod Johnson 73c06fd25e Fix display of error on join of collective 2018-10-08 09:54:03 -04:00
Jarrod Johnson 8d9a082739 Provide better exceptions and propogate them to client on snmp
When doing snmp, messages would always go to log only, even if the
user was at the confetty cli.  Give user access to knowing the error
impacting the query.
2018-10-04 14:59:25 -04:00
Jarrod Johnson 32602fbba3 Provide interactive handling of key mismatch in ssh sessions
Before, ssh would close without so much as a warning, fix this by
dealing with the key data.
2018-10-04 10:23:55 -04:00
Jarrod Johnson 2f616d4586 Better error when collective.manager is set to something invalid
If the collective.manager field does not correspond to any collective
manager, give a useful error rather than unexpected error.
2018-10-03 16:23:20 -04:00
Jarrod Johnson 15dc4937ee Add hyphen options to various completion 2018-10-02 14:44:25 -04:00
Jarrod Johnson 10cb1b77dd Extend the nodeboot man page 2018-10-02 14:36:07 -04:00
Jarrod Johnson d86e1fc4eb Give the cfg init a lock
Move collective manager and configmanager to share a configinitlock,
so that bad timings during internal initialization and collective
activity cannot interfere and produce corrupt database.

This became an issue with the fix for 'everything' disappearing.
2018-10-02 10:17:44 -04:00
Jarrod Johnson 29fcd59f8b Fix usage of check_quorum()
It is not a boolean, it is exception driven.
2018-10-01 16:02:45 -04:00
Jarrod Johnson 78a1741e0e Fix usage of check_quorum()
It is not a boolean, it is exception driven.
2018-10-01 16:02:16 -04:00
Jarrod Johnson 4329c1d388 Have collective start bail out if leader
Leader should not relinquish if quorum, so don't bother in such
a case.
2018-10-01 15:50:49 -04:00
Jarrod Johnson c13ebe36ae Have collective start bail out if leader
Leader should not relinquish if quorum, so don't bother in such
a case.
2018-10-01 15:50:31 -04:00
Jarrod Johnson 9495a41f85 Cancel retry if we become leader
If an instance is first to start, it's retry should be canceled
when other members prod it to become leader.
2018-10-01 15:29:42 -04:00
Jarrod Johnson b0b5493ff7 Cancel retry if we become leader
If an instance is first to start, it's retry should be canceled
when other members prod it to become leader.
2018-10-01 15:29:18 -04:00
Jarrod Johnson 484435c732 Fix /networking/macs/by-mac
The module apimacmap was not correctly scoped.
2018-10-01 14:40:25 -04:00
Jarrod Johnson 326f56219b Fix /networking/macs/by-mac
The module apimacmap was not correctly scoped.
2018-10-01 14:40:02 -04:00
Jarrod Johnson e098c0ba91 Fix missing tenant argument on user management function
The tenant was omitted preventing those particular rpc calls from
working correctly.
2018-10-01 14:04:03 -04:00
Jarrod Johnson a5d5f4a6a4 Fix missing tenant argument on user management function
The tenant was omitted preventing those particular rpc calls from
working correctly.
2018-10-01 14:03:45 -04:00
Jarrod Johnson 61e7c90ad1 Do not restart on intentional kill
Additionally, add some output to help filter events log
2018-10-01 10:32:55 -04:00
Jarrod Johnson 51194b275b Do not restart on intentional kill
Additionally, add some output to help filter events log
2018-10-01 09:35:02 -04:00
Jarrod Johnson e57cdf9a7b Add more collective event log handling
More detail to analyze how the collective membership is handled.
2018-09-27 15:15:05 -04:00
Jarrod Johnson e634817b34 Add more collective event log handling
More detail to analyze how the collective membership is handled.
2018-09-27 15:14:45 -04:00
Jarrod Johnson 10ce7a9de9 Add more logging to collective process 2018-09-27 10:51:06 -04:00
Jarrod Johnson 0724ad812b Add logging to the assimilation phase of collective
When attempting assimilation, provide logging about the attempt.
2018-09-27 10:51:01 -04:00
Jarrod Johnson a3b0b0240d Abort assimilation attempt on non-member cleanly
If a confluent instance has forgotten the collective, more cleanly
handle the situation, and abort the assimilation rather than assuming
the peer should be leader, unless txcount specifically is called out
as the reason.
2018-09-27 10:50:54 -04:00
Jarrod Johnson 7f06c69d3c Add more logging to collective process 2018-09-27 10:49:57 -04:00
Jarrod Johnson b5af770f21 Add logging to the assimilation phase of collective
When attempting assimilation, provide logging about the attempt.
2018-09-27 10:38:00 -04:00
Jarrod Johnson 247650868e Abort assimilation attempt on non-member cleanly
If a confluent instance has forgotten the collective, more cleanly
handle the situation, and abort the assimilation rather than assuming
the peer should be leader, unless txcount specifically is called out
as the reason.
2018-09-27 10:30:30 -04:00
Jarrod Johnson 9e8b2dd973 Begin storage configuration support
Start by adding a storage oriented configuration area, and have it be
able to list and show detail on disks.
2018-09-26 15:29:29 -04:00
Jarrod Johnson 99fdb20f87 Add nodemedia specific completion 2018-09-21 14:22:46 -04:00
Jarrod Johnson 60bb4c89fb Add specific completions for nodeboot and nodesetboot 2018-09-21 14:09:49 -04:00
Jarrod Johnson 5d52fd2fc1 Add nodeidentify specific completion
Additionally refactor common code into a function
2018-09-21 13:54:43 -04:00
Jarrod Johnson 18bebde337 Disable gssapi in paramiko
It is just plain broken, workaround by tanking calls to gssapi prior
to pulling in paramiko.
2018-09-21 13:46:07 -04:00
Jarrod Johnson c68c4d8cf7 Fix the 'list' subcommand of nodefirmware
nodefirmware did not properly set up if 'list' was given
2018-09-21 10:41:15 -04:00
Jarrod Johnson 1de84f0417 Add missing nodesupport man page 2018-09-21 09:57:34 -04:00
Jarrod Johnson 44bf2872b7 Provide more tailored completion for some commands
Additionally, modify the nodefirmware command to have a `list`
subcommand, so that tab completion doesn't aggressively send
someone to update.
2018-09-21 09:57:17 -04:00
Jarrod Johnson c209010126 Add tab completion for bash
For interactive bash use, have some completions
2018-09-20 16:02:45 -04:00
Jarrod Johnson 21b4a2f6f3 Merge branch 'master' of github.com:jjohnson42/confluent 2018-09-20 11:05:24 -04:00
Jarrod Johnson 36fc23d692 Avoid VT control codes on exit through pipe
Piped commands were subjected to terminal control sequences that
could interfere with desired operation.
2018-09-20 11:05:00 -04:00
Jarrod Johnson f601032a66 Fix everything group missing if nodegroup created before node
everything group was not making it to disk unless a node is created
first.  Correctly mark the need for disk sync to fix.
2018-09-14 16:50:20 -04:00
Jarrod Johnson 7c550bd68e Fix prefix fixup
It was not allowing same label across nodes.
2018-09-10 15:09:50 -04:00
Jarrod Johnson db5f861dc5 Fix introduced typo in error message 2018-09-10 14:25:04 -04:00
Jarrod Johnson d04be19ae5 Preferentially use a 'name' subfield as 'name'
Pyghmi now may suggest a more useful name.  The component name
is unique, but 'name' can indicate the common name of things with
multiple instances.
2018-09-07 14:37:02 -04:00
Jarrod Johnson 07532e2a3f Have nodeinventory disambiguate duplicate labels.
The data is still there for putting identical cards together, but
the prefix is unique, particularly important for json mode.
2018-09-07 11:49:13 -04:00
Jarrod Johnson e7be24d478 Revert "Fix non-unique name for similar inventory items."
This reverts commit 47a53a51e4.
2018-09-07 11:44:01 -04:00
Jarrod Johnson 34b7abcb2d Change systemd unit to not have PIDFile
systemctl restart *always* prints a worrying message
with pidfile.
2018-09-07 11:27:43 -04:00
Jarrod Johnson 47a53a51e4 Fix non-unique name for similar inventory items. 2018-09-07 11:16:09 -04:00
Jarrod Johnson abc15974da Merge branch 'master' of github.com:jjohnson42/confluent 2018-09-07 11:11:26 -04:00
Jarrod Johnson b3bf6929df Add replacement logic for another generic variant
In IMM, PCeGen3 x8 and similar is also possible.
2018-09-06 16:16:26 -04:00
Jarrod Johnson 2a8d61ecf6 Enrich the less than useful 'Adapter' inventory items
We can provide DNS provided info about such generic items to
make them look more fleshed out.
2018-09-06 16:10:48 -04:00
Jarrod Johnson cf3e9037ab Provide 'discovery.passwordrules'
This provides an ability to designate the desired rules that
are applied in the wake of automatic discovery.  The most popular
would be 'expiry=no,loginfailures=0'
2018-09-05 15:50:36 -04:00
Jarrod Johnson 03135543a6 Add 'switchuser' and 'switchpass' aliases 2018-09-05 13:51:19 -04:00
Jarrod Johnson 38228ebc9b Fix the prompting code changes 2018-09-04 11:09:26 -04:00
Jarrod Johnson d6110c7118 Add -p to nodegroupattrib
Also modify the man pages to reflect the updates.
2018-09-04 09:56:51 -04:00
Jarrod Johnson f92b1ed4a3 Implement ability to prompt for nodeattrib options.
For certain attributes, notably passwords, it is sometimes desirable
to prompt interactively to help facilitate keeping such data out of
bash_history, screen sharing, and ps output.  -e enables this if the
user is aware of how to use 'read', -p is a quicker way to enable this.
2018-09-04 09:38:01 -04:00
Jarrod Johnson 368087fb51 Have nodeattrib and nodeconfig accept wildcard to select values 2018-08-30 10:14:03 -04:00
Jarrod Johnson 46d62e67de Do not include advanced in comparedefault by default
Advanced settings may do unusual things, only check if explicitly
requested to do so.
2018-08-30 09:58:15 -04:00
Jarrod Johnson 118d1aec0d Allow nodeboot to harmlessly take -u
It doesn't actually change behavior, but request for compatibility
is being honored.
2018-08-30 09:39:20 -04:00
Jarrod Johnson 7c9089c87d Change nodeconfig -r to take a parameter
This opens up for future ability to control the restore to default.
2018-08-30 09:35:16 -04:00
Jarrod Johnson ba18b9936f Fix mistakes in previous commit 2018-08-29 15:15:34 -04:00
Jarrod Johnson 3b7ecd0095 Add ability to clear system configuration
This provides a method to request the system firmware be restored to
factory defaults.
2018-08-29 14:49:19 -04:00
Jarrod Johnson 19e9583b47 Fix formatting on node*define man page sources 2018-08-28 15:08:03 -04:00
Jarrod Johnson 8352007570 Limit to one active scan at a time
Additionally, provide read access to rescan for discovery.
2018-08-28 11:25:48 -04:00
Jarrod Johnson f7965d235a Improve /networking/macs API behavior
For the 'by-mac', only remove the structure when it is ready for API
view without changing internal view.

For the 'by-switch', do the update per switch and after it's done.

Provide ability to check scan status through reading
/networking/macs/rescan
2018-08-28 11:10:32 -04:00
Jarrod Johnson 6aec9534e7 Fixes for nodesupport 2018-08-23 16:56:40 -04:00
Jarrod Johnson 3ee6334db2 Fix transfer owner argument for servicedata 2018-08-23 16:48:10 -04:00
Jarrod Johnson 582a4de62d Add CLI and directory support for nodesupport 2018-08-23 16:36:41 -04:00
Jarrod Johnson c9959d4082 More of the service data retrieval api
More progress is made toward the goal
2018-08-23 16:14:49 -04:00
Jarrod Johnson fa11fb54cb Add API support for getting service data
Service data retrieval is a common activity required
for interacting with support.
2018-08-23 15:39:25 -04:00
Jarrod Johnson ee3b824870 Add /description to nodes api 2018-08-23 08:44:41 -04:00
Jarrod Johnson 55f5b30369 Merge branch '21' 2018-08-22 16:29:58 -04:00
Jarrod Johnson 784e4bed2f Force cleanup if follow thread dies of exception
If something killed a follow thread, it was not always able to fire the
recovery off.  Wrap the risky code in a try statement.
2018-08-20 15:02:34 -04:00
Jarrod Johnson df7cba00fd Amend the message on collective failure 2018-08-17 16:45:45 -04:00
Jarrod Johnson dfb720d0ee Have collective command warn if the libssl library is not viable
Main example is RedHat providing pyOpenSSL of relatively ancient
vintage.
2018-08-17 13:57:13 -04:00
Jarrod Johnson f5d5cbd67b Have collective command warn if the libssl library is not viable
Main example is RedHat providing pyOpenSSL of relatively ancient
vintage.
2018-08-17 13:56:38 -04:00
Jarrod Johnson 319fec2145 Add advanced to nodeconfig 2018-08-17 11:16:11 -04:00
Jarrod Johnson 8787d23b3a Add advanced to API for system configuration.
pyghmi makes hiding optional advanced settings.
2018-08-17 10:59:50 -04:00
Jarrod Johnson 9b48110155 Do not proceed a logged, but broken session
It shouldn't be possible for this to be the case, but out of an
abundance of caution, check for this.  So far only produced this by
forcing broken = True in a debug session.  Intended to catch an alleged
scenario where console was managing to use a broken session (fixed in
pyghmi) and have confluent also recognize the situation for non-console
usage).
2018-08-16 14:43:16 -04:00
Jarrod Johnson 3064e7bef6 Ensure path is made prior to creating transactioncount
Fresh install will be missing /etc/confluent/cfg.  Advance the
_mkpath call to fix this problem.
2018-08-08 18:05:45 -04:00
Jarrod Johnson 1d4df8af3a Fix extraneous error in log on connectivity loss 2018-08-07 15:43:53 -04:00
Jarrod Johnson 2aba6e469c Correct variable name in the 'connected' fix 2018-08-07 15:31:41 -04:00
Jarrod Johnson de58593f14 Fix inability to notice underlying broken layers of the SOL
Through an unknown set of circumstances, an solconnection could be
stuck 'connecting'.  In every case analyzed, the ipmi_session was
broken.  Use that to detect a class of failure and react appropriately.
2018-08-07 15:12:53 -04:00
Jarrod Johnson ecbe1a86b1 Revert "Have nodeconsole restore term on exit"
This reverts commit 2972374da8.
2018-08-02 10:27:37 -04:00
Jarrod Johnson 2972374da8 Have nodeconsole restore term on exit 2018-08-02 10:07:41 -04:00
Jarrod Johnson 81dd6202d3 Fix when rpc has no 'exc' but has 'xid' 2018-07-30 11:26:09 -04:00
Jarrod Johnson 36a202842a Fix collective on rpc exception
Exceptions on collective calls were not correctly handled, fix
the handling so that collective continues and also the calling function
is correctly given the exception.
2018-07-30 09:33:24 -04:00
Jarrod Johnson 6a8e24dd0e Prioritize interactive feedback part of console handling. 2018-07-26 08:55:25 -04:00
Jarrod Johnson d3afeb3414 Fix web shell if user hits enter too fast 2018-07-24 17:20:22 -04:00
Jarrod Johnson 1bf4c0ac0a Have collective coalesce watched updates
Particularly chatty output can make collate be unreasonable in
low quality terminals and links.  Throttle to about 4 times a second.
2018-07-24 16:50:46 -04:00
Jarrod Johnson 8e422ef822 Fix ssh access
Fixed handler (e.g. ssh) did not return console consistent with
the plugin defined handlers.
2018-07-24 16:48:46 -04:00
Jarrod Johnson f0edbbad39 Have collective show present some info when not in quorum 2018-07-20 14:11:38 -04:00
Jarrod Johnson 5cf1671350 Make the takeover process more deterministic
Try to avoid submitting to be a follower while we are currently
becoming a leader
2018-07-20 13:50:42 -04:00
Jarrod Johnson e5c4219ee9 Reorder certificate check
First order of business is to verify certificate before even thinking
about if the request is possible
2018-07-20 13:34:14 -04:00
Jarrod Johnson 3ff7e42074 Change behavior for fallback handling
Fallback would do nothing to fix a persistent problem with an IPMI
session.  For lack of knowing how to avoid the situation, at least
make changes so it won't go wrong in the future.
2018-07-20 13:20:50 -04:00
Jarrod Johnson fab177e077 Fix node[group][attrib|define] handling of =
Attributes with = in the value were not handled correctly,
fix by only doing one split.
2018-07-20 09:54:17 -04:00
Jarrod Johnson a1ba5f59a8 Fix collective show on non-collective 2018-07-19 17:21:01 -04:00
Jarrod Johnson 9bcca6bfad Provide collective show on all members 2018-07-19 17:08:20 -04:00
Jarrod Johnson 96671ace4e Correct collective show behavior 2018-07-19 16:48:30 -04:00
Jarrod Johnson bcff3fc962 Improve collective show readability 2018-07-19 16:39:13 -04:00
Jarrod Johnson 54d93571d1 Have leader provide more data in collective show 2018-07-19 16:26:05 -04:00
Jarrod Johnson f2f902de7b Have collective show report when collective inactive
Collective show was misleading if not in a collective.
2018-07-19 15:59:15 -04:00
Jarrod Johnson a09792f969 Schedule periodic attempts to restart collective
If collective is lost due to connectivity, this will cause
occasional attempts to bring it back.
2018-07-19 15:49:05 -04:00
Jarrod Johnson 7d16c943a8 Handle updating address of collective member on connect
If a collective member changes its IP address, update at the next
possible opportunity.
2018-07-19 15:24:08 -04:00
Jarrod Johnson b053d41cd8 Error on loss of manager in flight 2018-07-19 14:36:23 -04:00
141 changed files with 9622 additions and 7837 deletions
+33 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -21,6 +21,7 @@
import optparse
import os
import select
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -34,6 +35,9 @@ import confluent.client
argparser = optparse.OptionParser(usage="Usage: <other command> | %prog [options]")
argparser.add_option('-a', '--abbreviate', action='store_true',
help='Attempt to use confluent server to shorten noderanges')
argparser.add_option('-b', '--base',
help='Use given node as reference for comparison when '
'using -d, instead of using the most common result')
argparser.add_option('-d', '--diff', action='store_true',
help='Show what differs between most common '
'output group and others')
@@ -47,6 +51,8 @@ argparser.add_option('-c', '--count', action='store_true',
argparser.add_option('-r', '--reverse', action='store_true',
help='Reverse sort order to show biggest output group '
'last')
argparser.add_option('-l', '--log', action='store', type='string', dest='log',
help='Log each output to file, using {node} as a placeholder for node.')
(options, args) = argparser.parse_args()
if sys.stdin.isatty():
argparser.print_help()
@@ -62,7 +68,7 @@ else:
def print_current():
if options.diff:
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
reverse=options.reverse)
reverse=options.reverse, basenode=options.base)
else:
grouped.print_all(skipmodal=options.skipcommon,
count=options.count,
@@ -70,17 +76,39 @@ def print_current():
sys.stdout.flush()
fullline = sys.stdin.readline()
printpending = True
clearpending = False
holdoff = 0
while fullline:
for line in fullline.split('\n'):
if not line:
continue
if ': ' not in line:
line = 'UNKNOWN: ' + line
if options.log:
node, output = line.split(': ', 1)
currlog = options.log.format(node=node, nodename=node)
with open(currlog, mode='a') as log:
log.write(output + '\n')
continue
grouped.add_line(*line.split(': ', 1))
if options.watch:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
if not holdoff:
holdoff = os.times()[4] + 0.250
if (holdoff < os.times()[4] or
not select.select((sys.stdin,), (), (), 0.250)[0]):
# print now, nothing pending
holdoff = 0
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
printpending = False
clearpending = True
else:
printpending = True
fullline = sys.stdin.readline()
if not options.watch:
if printpending:
if clearpending:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
+194 -135
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ import select
import shlex
import signal
import socket
import struct
import sys
import time
try:
@@ -88,6 +89,15 @@ except NameError:
netserver = None
laststate = {}
try:
input = raw_input
except NameError:
pass
class BailOut(Exception):
def __init__(self, errorcode=0):
self.errorcode = errorcode
def print_help():
print("confetty provides a filesystem like interface to confluent. "
@@ -189,9 +199,9 @@ def prompt():
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
try:
return raw_input(target + ' -> ')
return input(target + ' -> ')
except KeyboardInterrupt:
print ""
print("")
return ""
except EOFError: # ctrl-d
print("exit")
@@ -290,7 +300,7 @@ currchildren = None
def print_result(res):
if 'errorcode' in res or 'error' in res:
print res['error']
print(res['error'])
return
if 'databynode' in res:
print_result(res['databynode'])
@@ -303,9 +313,9 @@ def print_result(res):
attrstr = '%s=%s' % (key, recurse_format(res[key]))
elif not isinstance(res[key], dict):
try:
print '{0}: {1}'.format(key, res[key])
print('{0}: {1}'.format(key, res[key]))
except UnicodeEncodeError:
print '{0}: {1}'.format(key, repr(res[key]))
print('{0}: {1}'.format(key, repr(res[key])))
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
@@ -318,7 +328,7 @@ def print_result(res):
else:
sys.stdout.write('{0}: '.format(key))
if isinstance(res[key], str) or isinstance(res[key], unicode):
print res[key]
print(res[key])
else:
print_result(res[key])
continue
@@ -353,7 +363,7 @@ def do_command(command, server):
if argv[0] == 'exit':
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(0)
raise Bailout()
elif argv[0] in ('help', '?'):
return print_help()
elif argv[0] == 'cd':
@@ -417,10 +427,10 @@ def do_command(command, server):
for res in session.read(targpath):
if 'item' in res: # a link relation
if type(res['item']) == dict:
print res['item']["href"]
print(res['item']["href"])
else:
for item in res['item']:
print item["href"]
print(item["href"])
else: # generic attributes to list
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
@@ -434,6 +444,10 @@ def do_command(command, server):
currconsole = targpath
startrequest = {'operation': 'start', 'path': targpath,
'parameters': {}}
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
startrequest['parameters']['width'] = width
startrequest['parameters']['height'] = height
for param in argv[2:]:
(parmkey, parmval) = param.split("=")
startrequest['parameters'][parmkey] = parmval
@@ -478,7 +492,8 @@ def createresource(args):
collection = targpath
else:
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
if 'name' not in keydata:
keydata['name'] = resname
makecall(session.create, (collection, keydata))
@@ -493,6 +508,13 @@ def makecall(callout, args):
if 'errorcode' in response:
exitcode = response['errorcode']
sys.stderr.write('Error: ' + response['error'] + '\n')
if 'databynode' in response:
lresponse = response['databynode']
for node in lresponse:
if 'errorcode' in lresponse[node]:
exitcode = lresponse[node]['errorcode']
if 'error' in lresponse[node]:
sys.stderr.write('{0}: Error - {1}\n'.format(node, lresponse[node]['error']))
def clearvalues(resource, attribs):
@@ -575,11 +597,20 @@ def fullpath_target(currpath, forcepath=False):
ntarget += '/'
return ntarget
def do_resize(a, b):
if not inconsole:
return
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
'height': height})
def startconsole(nodename):
global inconsole
global consolename
global didconsole
signal.signal(signal.SIGWINCH, do_resize)
didconsole = True
consolename = nodename
tty.setraw(sys.stdin.fileno())
@@ -598,11 +629,12 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
if oldtcattr is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
# Request default color scheme, to undo potential weirdness of terminal
sys.stdout.write('\x1b[m')
if sys.stdout.isatty():
sys.stdout.write('\x1b[m')
if fullexit:
if os.environ.get('TERM', '') not in ('linux'):
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(code)
raise BailOut(code)
else:
tlvdata.send(session.connection, {'operation': 'stop',
'path': currconsole})
@@ -795,6 +827,10 @@ parser.add_option("-s", "--server", dest="netserver",
parser.add_option("-c", "--control", dest="controlpath",
help="Path to offer terminal control",
metavar="PATH")
parser.add_option(
'-m', '--mintime', default=0,
help='Minimum time to run or else pause for input (used to keep a '
'terminal from closing quickly on error)')
opts, shellargs = parser.parse_args()
username = None
@@ -819,49 +855,10 @@ def server_connect():
passphrase = os.environ['CONFLUENT_PASSPHRASE']
session.authenticate(username, passphrase)
while not session.authenticated:
username = raw_input("Name: ")
username = input("Name: ")
passphrase = getpass.getpass("Passphrase: ")
session.authenticate(username, passphrase)
try:
server_connect()
except EOFError, KeyboardInterrupt:
sys.exit(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
sys.exit(1)
# clear on start can help with readable of TUI, but it
# can be annoying, so for now don't do it.
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
if sys.stdout.isatty():
import readline
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
dl = readline.get_completer_delims().replace('-', '')
readline.set_completer_delims(dl)
readline.set_completer(completer)
doexit = False
inconsole = False
pendingcommand = ""
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
command = " ".join(shellargs)
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
powertime = None
def check_power_state():
tlvdata.send(
session.connection,
@@ -870,86 +867,148 @@ def check_power_state():
return
while inconsole or not doexit:
if inconsole:
try:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 10)
except select.error:
rdylist = ()
for fh in rdylist:
if fh == session.connection:
# this only should get called in the
# case of a console session
# each command should slurp up all relevant
# recv potential
#fh.read()
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
updatestatus(data)
continue
if data is not None:
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
try:
sys.stdout.write(data)
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
sys.stdout.flush()
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
try:
myinput = fh.read()
myinput = check_escape_seq(myinput, fh)
if myinput:
tlvdata.send(session.connection, myinput)
except IOError:
pass
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
powertime = time.time()
powerstate = True
check_power_state()
else:
currcommand = prompt()
try:
do_command(currcommand, netserver)
except socket.error:
if sys.stdout.isatty():
import readline
def main():
global inconsole
try:
server_connect()
except (EOFError, KeyboardInterrupt) as _:
raise BailOut(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
raise BailOut(1)
# clear on start can help with readable of TUI, but it
# can be annoying, so for now don't do it.
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
global powerstate, powertime, clearpowermessage
if sys.stdout.isatty():
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
dl = readline.get_completer_delims().replace('-', '')
readline.set_completer_delims(dl)
readline.set_completer(completer)
doexit = False
inconsole = False
pendingcommand = ""
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
command = " ".join(shellargs)
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
powertime = None
while inconsole or not doexit:
if inconsole:
try:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 10)
except select.error:
rdylist = ()
for fh in rdylist:
if fh == session.connection:
# this only should get called in the
# case of a console session
# each command should slurp up all relevant
# recv potential
#fh.read()
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
updatestatus(data)
continue
if data is not None:
data = client.stringify(data)
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
try:
sys.stdout.write(data)
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
sys.stdout.flush()
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
try:
myinput = fh.read()
myinput = check_escape_seq(myinput, fh)
if myinput:
tlvdata.send(session.connection, myinput)
except IOError:
pass
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
powertime = time.time()
powerstate = True
check_power_state()
else:
currcommand = prompt()
try:
server_connect()
do_command(currcommand, netserver)
except socket.error:
doexit = True
sys.stdout.write('Lost connection to server')
quitconfetty(fullexit=True)
try:
server_connect()
do_command(currcommand, netserver)
except socket.error:
doexit = True
sys.stdout.write('Lost connection to server')
quitconfetty(fullexit=True)
if __name__ == '__main__':
errcode = 0
deadline = 0
if opts.mintime:
deadline = os.times()[4] + float(opts.mintime)
try:
main()
except BailOut as e:
errcode = e.errorcode
finally:
if deadline and os.times()[4] < deadline:
sys.stderr.write('[Exited early, hit enter to continue]')
sys.stdin.readline()
if errcode == 0:
errcode = exitcode
sys.exit(errcode)
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/python2
import optparse
import signal
import sys
import os
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
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.sortutil as sortutil
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o ansible.hosts
\n ''')
argparser.add_option('-o', '--output',
help='Ansible hosts file')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not options.output:
sys.stderr.write('Output file must be specified by -o\n')
sys.exit(1)
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
nodesbygroup = {}
with open(options.output, 'w') as importfile:
needempty = False
for node in sortutil.natural_sort(databynode):
data = databynode[node]
if not data.get('groups', []):
importfile.write(node + '\n')
needempty = True
for g in data.get('groups', []):
if g not in nodesbygroup:
nodesbygroup[g] = set([node])
else:
nodesbygroup[g].add(node)
if needempty:
importfile.write('\n')
for group in sortutil.natural_sort(nodesbygroup):
importfile.write('[{0}]\n'.format(group))
for node in sortutil.natural_sort(nodesbygroup[group]):
importfile.write('{0}\n'.format(node))
importfile.write('\n')
if __name__ == '__main__':
main()
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/python2
import csv
import optparse
import signal
import sys
import os
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
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.sortutil as sortutil
headers = '# Type,Serial Number,Current IP,Current username,Current password,New password,Recovery password,Switch enable password,New IPv4,IPv4 Subnet mask,IPv4 Default gateway,IPv4 DNS1,IPv4 DNS2,New IPv6,IPv6 Prefix,IPv6 Default gateway,IPv6 DNS1,IPv6 DNS2,Domain,Host name,Display name,Rack,Lowest Rack Unit,Height,Force,Stored credentials ID,Managed authentication,Group Name'.split(',')
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o bulkimport.csv
\n ''')
argparser.add_option('-o', '--output',
help='File to write for bulk import into xClarity Administrator')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not options.output:
sys.stderr.write('Output file must be specified by -o\n')
sys.exit(1)
if 'XCCUSER' not in os.environ or 'XCCPASS' not in os.environ:
sys.stderr.write('Must specify XCCUSER and XCCPASS in environment variables\n')
sys.exit(1)
xccuser = os.environ['XCCUSER']
xccpass = os.environ['XCCPASS']
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
for res in sess.read(
'/noderange/{0}/configuration/management_controller/net_interfaces/management'.format(
noderange)):
for node in res.get('databynode', {}):
currip = res['databynode'][node].get('ipv4_address', {}).get(
'value', '')
if currip:
databynode[node]['hardwaremanagement.manager'] = {
'value': currip.split('/', 1)[0]}
with open(options.output, 'w') as importfile:
bulkimport = csv.writer(importfile)
bulkimport.writerow(headers)
for node in sortutil.natural_sort(databynode):
data = databynode[node]
row = ['server', lookupdata(data, 'id.serial'), lookupdata(data, 'hardwaremanagement.manager'), xccuser, xccpass, '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', node, lookupdata(data, 'location.rack'), lookupdata(data, 'location.u'), '', '', '','', '']
bulkimport.writerow(row)
if __name__ == '__main__':
main()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/python2
import csv
import optparse
import signal
import sys
import os
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
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.sortutil as sortutil
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o xcatnodes.def
\n ''')
argparser.add_option('-o', '--output',
help='xCAT stanza file')
argparser.add_option('-m', '--macs',
help='xCAT macs.csv to write')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not (options.output or options.macs):
sys.stderr.write('Output file must be specified by -o or -m\n')
sys.exit(1)
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
if options.output:
with open(options.output, 'w') as importfile:
for node in sortutil.natural_sort(databynode):
xcatattrs = collect_attribute_data(databynode, node)
importfile.write('{0}:\n'.format(node))
importfile.write(' objtype=node\n')
importfile.write(' arch=x86_64\n')
importfile.write(' netboot=xnba\n')
importfile.write(' mgt=ipmi\n')
for attr in xcatattrs:
if xcatattrs[attr] is None:
continue
importfile.write(' {0}={1}\n'.format(attr,
xcatattrs[attr]))
importfile.write('\n')
if options.macs:
with open(options.macs, 'w') as importfile:
macsv = csv.writer(importfile)
macsv.writerow(['#node', 'mac'])
for node in sortutil.natural_sort(databynode):
xcatattrs = collect_attribute_data(databynode, node)
macsv.writerow([node, xcatattrs['mac']])
def collect_attribute_data(databynode, node):
data = databynode[node]
xcatattrs = {}
xcatattrs['groups'] = ','.join(data.get('groups', []))
xcatattrs['bmc'] = data.get('hardwaremanagement.manager', {}).get(
'value', None)
xcatattrs['mpa'] = data.get('enclosure.manager', {}).get(
'value', None)
xcatattrs['slotid'] = data.get('enclosure.bay', {}).get(
'value', None)
gotmac = False
for key in data:
if key.startswith('net.') and 'hwaddr' in key:
currmac = data[key].get('value', None)
if currmac:
if gotmac:
sys.stderr.write(
'Ignoring {0} and using only {1} for mac, '
'multiple macs not supported by '
'confluent2xcat\n'.format(key, gotmac))
continue
gotmac = key
xcatattrs['mac'] = currmac
return xcatattrs
if __name__ == '__main__':
main()
+24 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -17,6 +17,7 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -47,6 +48,11 @@ argparser.add_option('-e', '--environment', action='store_true',
'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')
(options, args) = argparser.parse_args()
@@ -67,12 +73,25 @@ exitcode = 0
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment:
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)
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
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)
try:
# setting user output to what the user inputs
if args[1] == 'all':
@@ -111,6 +130,6 @@ else:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
sys.exit(exitcode)
+5 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,6 +32,8 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
argparser.add_option('-m', '--maxnodes', type='int',
help='Number of nodes to affect before prompting for confirmation')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -43,7 +45,7 @@ session = client.Command()
exitcode = 0
errorNodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
@@ -56,7 +58,7 @@ for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": BMC Reset Successful"
print(node + ": BMC Reset Successful")
sys.exit(success)
+9 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -35,10 +35,17 @@ argparser = optparse.OptionParser()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-u', '--uefi', dest='uefimode',
action='store_true', default=False,
help='Request UEFI style boot (rather than BIOS)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to boot, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
@@ -63,6 +70,7 @@ else:
bootmode = 'uefi'
errnodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist,
+136 -58
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -19,6 +19,7 @@
import os
import signal
import optparse
import shlex
import sys
try:
@@ -42,11 +43,13 @@ def bailout(msg, code=1):
sys.exit(code)
argparser = optparse.OptionParser()
argparser = optparse.OptionParser(usage="Usage: %prog [options] noderange [option|option=value]")
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
action='store_true', default=False,
help='Compare given settings to default or list settings '
'that are non default')
argparser.add_option('-b', '--batch', dest='batch', metavar='settings.batch',
default=False, help='Provide settings in a batch file')
argparser.add_option('-d', '--detail', dest='detail',
action='store_true', default=False,
help='Provide verbose information as available, such as '
@@ -55,6 +58,20 @@ argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
'examine, compare, or restore default')
argparser.add_option('-a', '--advanced', dest='advanced',
action='store_true', default=False,
help='Include advanced settings, which are normally not '
'intended to be used without direction from the '
'relevant server vendor.')
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')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to configure, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
cfgpaths = {
@@ -85,6 +102,8 @@ setmode = None
assignment = {}
queryparms = {}
printsys = []
printbmc = []
printallbmc = False
setsys = {}
forceset = False
needval = None
@@ -108,64 +127,110 @@ def _assign_value():
assignment[key] = value
for param in args[1:]:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
setmode = True
forceset = True
continue
if needval:
key = needval
value = param
_assign_value()
continue
if '=' in param or param[-1] == ':' or forceset:
if setmode is None:
def parse_config_line(arguments):
global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
for param in arguments:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
setmode = True
if setmode != True:
bailout('Cannot do set and query in same command')
if '=' in param:
key, _, value = param.partition('=')
forceset = True
continue
if needval:
key = needval
needval = None
value = param
_assign_value()
elif param[-1] == ':':
needval = param[:-1]
continue
if '=' in param or param[-1] == ':' or forceset:
if setmode is None:
setmode = True
if setmode != True:
bailout('Cannot do set and query in same command')
if '=' in param:
key, _, value = param.partition('=')
_assign_value()
elif param[-1] == ':':
needval = param[:-1]
else:
needval = param
else:
needval = param
else:
if setmode is None:
setmode = False
if setmode != False:
bailout('Cannot do set and query in same command')
if '.' not in param:
matchedparms = False
for candidate in cfgpaths:
if candidate.startswith('{0}.'.format(param)):
matchedparms = True
if not options.exclude:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
else:
try:
del queryparms[path]
except KeyError:
pass
if not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = param
if setmode is None:
setmode = False
if setmode != False:
bailout('Cannot do set and query in same command')
if '.' not in param:
if param == 'bmc':
printallbmc = True
matchedparms = False
for candidate in cfgpaths:
if candidate.startswith('{0}.'.format(param)):
matchedparms = True
if not options.exclude:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
else:
try:
del queryparms[path]
except KeyError:
pass
if not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
if param.startswith('bmc.'):
printbmc.append(param.replace('bmc.', ''))
else:
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
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))
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)
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)
@@ -173,8 +238,12 @@ if setmode:
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
path = 'configuration/system/all'
attrib = key
if key.startswith('bmc.'):
path = 'configuration/management_controller/extended/all'
attrib = key.replace('bmc.', '')
else:
path = 'configuration/system/all'
attrib = key
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
@@ -206,10 +275,19 @@ else:
NullOpt(), queryparms[path])
if rc:
sys.exit(rc)
if printbmc or printallbmc:
rcode = client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
session, printbmc, options, attrprefix='bmc.')
if printsys or options.exclude:
if printsys == 'all':
printsys = []
path = '/noderange/{0}/configuration/system/all'.format(noderange)
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)
sys.exit(rcode)
+46 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -17,7 +17,14 @@
import optparse
import os
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
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.sortutil as sortutil
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
argparser = optparse.OptionParser(
@@ -26,9 +33,45 @@ argparser = optparse.OptionParser(
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
"For example, ctrl-'e', then 'c', then '.' will exit the current "
"console")
argparser.add_option('-t', '--tile', action='store_true', default=False,
help='Tile console windows in the terminal')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
if options.tile:
null = open('/dev/null', 'w')
nodes = []
sess = client.Command()
for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
node = res.get('item', {}).get('href', '/').replace('/', '')
if not node:
sys.stderr.write(res.get('error', repr(res)) + '\n')
sys.exit(1)
nodes.append(node)
initial = True
pane = 0
for node in sortutil.natural_sort(nodes):
if initial:
initial = False
subprocess.call(
['tmux', 'new-session', '-d', '-s',
'nodeconsole_{0}'.format(os.getpid()), '-x', '800', '-y',
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
else:
subprocess.call(['tmux', 'select-pane', '-t', str(pane)])
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
pane += 1
subprocess.call(
['tmux', 'split', '-h',
'{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
subprocess.call(['tmux', 'select-layout', 'tiled'], stdout=null)
subprocess.call(['tmux', 'select-pane', '-t', '0'])
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
os.execlp('tmux', 'tmux', 'attach', '-t', 'nodeconsole_{0}'.format(
os.getpid()))
else:
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -47,7 +47,7 @@ session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
@@ -55,4 +55,4 @@ for r in session.create('/noderange/', attribs):
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
sys.exit(exitcode)
+112 -38
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -19,6 +19,7 @@ import csv
import optparse
import os
import sys
import time
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
@@ -26,31 +27,49 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
tabformat = '{0:>15}|{1:>15}|{2:>15}|{3:>36}|{4:>17}|{5:>12}|{6:>48}'
columns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
delimit = ['-' * 15, '-' * 15, '-' * 15, '-' * 36, '-' * 17, '-' * 12,
'-' * 48]
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
columnmapping = {
'Node': 'nodename',
'Model': 'modelnumber',
'Serial': 'serialnumber',
'UUID': 'uuid',
'Type': 'types',
'Current IP Addresses': 'ipaddrs',
'IP': 'ipaddrs',
'Bay': 'bay',
'Mac Address': 'macs',
'Mac': 'macs',
'Switch': 'switch',
'Port': 'port',
'Advertised IP': 'otheripaddrs',
'Other IP': 'otheripaddrs',
}
def dumpmacs(procinfo):
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
def print_disco(options, session, currmac):
def print_disco(options, session, currmac, outhandler, columns):
procinfo = {}
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
record = [procinfo['nodename'], procinfo['modelnumber'],
procinfo['serialnumber'], procinfo['uuid'], dumpmacs(procinfo),
','.join(procinfo['types']),
','.join(sorted(procinfo['ipaddrs']))]
if options.csv:
csv.writer(sys.stdout).writerow(record)
else:
print(tabformat.format(*record))
if 'Switch' in columns or 'Port' in columns:
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
# is last
procinfo.update(tmpinfo['ports'][-1])
record = []
for col in columns:
rawcol = columnmapping[col]
rawval = procinfo.get(rawcol, '')
if isinstance(rawval, list):
record.append(','.join(rawval))
else:
record.append(str(rawval))
outhandler.add_row(record)
def process_header(header):
@@ -72,7 +91,9 @@ def process_header(header):
elif datum in ('bmc', 'imm', 'xcc'):
fields.append('hardwaremanagement.manager')
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
fields.append('net.bmc.gateway')
fields.append('net.bmc.ipv4_gateway')
elif datum in ('bmc_gateway', 'xcc_gateway', 'imm_gateway'):
fields.append('net.bmc.ipv4_gateway')
elif datum in ('bmcuser', 'username', 'user'):
fields.append('secret.hardwaremanagementuser')
elif datum in ('bmcpass', 'password', 'pass'):
@@ -122,24 +143,44 @@ def datum_to_attrib(datum):
del datum['node']
return datum
unique_fields = frozenset(['serial', 'mac', 'uuid'])
def import_csv(options, session):
nodedata = []
unique_data = {}
with open(options.importfile, 'r') as datasrc:
records = csv.reader(datasrc)
fields = process_header(next(records))
for field in fields:
if field in unique_fields:
unique_data[field] = set([])
broken = False
for record in records:
currfields = list(fields)
nodedatum = {}
for datum in record:
nodedatum[currfields.pop(0)] = datum
currfield = currfields.pop(0)
if currfield in unique_fields:
if datum in unique_data[currfield]:
sys.stderr.write(
"Import contains duplicate values "
"({0} with value {1}\n".format(currfield, datum)
)
sys.exit(1)
unique_data[currfield].add(datum)
nodedatum[currfield] = datum
if not datum_complete(nodedatum):
sys.exit(1)
if not search_record(nodedatum, options, session) and not broken:
blocking_scan(session)
if not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
sys.exit(1)
broken = True
nodedata.append(nodedatum)
if broken:
sys.exit(1)
for datum in nodedata:
maclist = search_record(datum, options, session)
datum = datum_to_attrib(datum)
@@ -165,13 +206,28 @@ def import_csv(options, session):
def list_discovery(options, session):
if options.csv:
csv.writer(sys.stdout).writerow(columns)
orderby = None
if options.fields:
columns = []
for field in options.fields.split(','):
for cdt in columnmapping:
if cdt.lower().replace(
' ', '') == field.lower().replace(' ', ''):
columns.append(cdt)
else:
print(tabformat.format(*columns))
print(tabformat.format(*delimit))
columns = defcolumns
if options.order:
for field in columns:
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)
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):
@@ -181,8 +237,10 @@ def clear_discovery(options, session):
else:
print(repr(res))
def list_matching_macs(options, session):
def list_matching_macs(options, session, node=None):
path = '/discovery/'
if node:
path += 'by-node/{0}/'.format(node)
if options.model:
path += 'by-model/{0}/'.format(options.model)
if options.serial:
@@ -205,21 +263,25 @@ def list_matching_macs(options, session):
path += 'by-mac/'
return [x['item']['href'] for x in session.read(path)]
def assign_discovery(options, session):
def assign_discovery(options, session, needid=True):
abort = False
if options.importfile:
return import_csv(options, session)
if not (options.serial or options.uuid or options.mac):
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if needid and not (options.serial or options.uuid or options.mac):
sys.stderr.write(
"UUID (-u), serial (-s), or ether address (-e) required for "
"assignment\n")
abort = True
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if abort:
sys.exit(1)
matches = list_matching_macs(options, session)
matches = list_matching_macs(options, session, None if needid else options.node)
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)
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
@@ -230,6 +292,11 @@ def assign_discovery(options, session):
else:
print(repr(res))
def blocking_scan(session):
list(session.update('/discovery/rescan', {'rescan': 'start'}))
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
time.sleep(0.5)
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
def main():
@@ -266,8 +333,13 @@ def main():
parser.add_option('-i', '--import', dest='importfile',
help='Import bulk assignment data from given CSV file',
metavar='IMPORT.CSV')
parser.add_option('-f', '--fields', dest='fields',
help='Select fields for output',
metavar='FIELDS')
parser.add_option('-o', '--order', dest='order',
help='Order output by given field', metavar='ORDER')
(options, args) = parser.parse_args()
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
if len(args) == 0 or args[0] not in ('list', 'assign', 'reassign', 'rescan', 'clear'):
parser.print_help()
sys.exit(1)
session = client.Command()
@@ -277,10 +349,12 @@ def main():
clear_discovery(options, session)
if args[0] == 'assign':
assign_discovery(options, session)
if args[0] == 'reassign':
assign_discovery(options, session, False)
if args[0] == 'rescan':
list(session.update('/discovery/rescan', {'rescan': 'start'}))
print("Rescan initiated")
blocking_scan(session)
print("Rescan complete")
if __name__ == '__main__':
main()
main()
+19 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# 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.
@@ -33,10 +33,15 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [clear]")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to clear if clearing log, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -45,11 +50,11 @@ except IndexError:
sys.exit(1)
client.check_globbing(noderange)
deletemode = False
if len(sys.argv) > 3:
if len(args) > 2:
argparser.print_help()
sys.exit(1)
if len(sys.argv) == 3:
if sys.argv[2] == 'clear':
if len(args) == 2:
if args[1] == 'clear':
deletemode = True
else:
argparser.print_help()
@@ -64,6 +69,8 @@ def format_event(evt):
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
dscparts = []
if evt.get('log_id', None):
retparts.append(evt['log_id'] + ':')
if 'component_type' in evt and evt['component_type'] is not None:
dscparts.append(evt['component_type'])
if 'component' in evt and evt['component'] is not None:
@@ -77,11 +84,15 @@ def format_event(evt):
pass
dscparts.append(evttext)
retparts.append(' - '.join(dscparts))
return ' '.join(retparts)
msg = evt.get('message')
if not msg:
msg = ''
return ' '.join(retparts) + msg
if deletemode:
func = session.delete
session.stop_if_noderange_over(noderange, options.maxnodes)
else:
func = session.read
for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
@@ -98,4 +109,4 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
if 'events' in thisdata:
evtdata = thisdata['events']
for evt in evtdata:
print '{0}: {1}'.format(node, format_event(evt))
print('{0}: {1}'.format(node, format_event(evt)))
+21 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -37,14 +37,14 @@ exitcode = 0
def printfirm(node, prefix, data):
if 'model' in data:
if 'model' in data and data['model']:
prefix += ' ' + data['model']
builddesc = []
if 'build' in data:
if 'build' in data and data['build']:
builddesc.append(data['build'])
if 'date' in data:
if 'date' in data and data['date']:
builddesc.append(data['date'])
if 'version' in data:
if 'version' in data and data['version']:
version = data['version']
if builddesc:
version += ' ({0})'.format(' '.join(builddesc))
@@ -55,9 +55,14 @@ def printfirm(node, prefix, data):
components = ['all']
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [update [--backup <file>]]|[<components>]")
usage="Usage: "
"%prog <noderange> [list][update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
argparser.add_option('-m', '--maxnodes', type='int',
help='When updating, prompt if more than the specified '
'number of servers will be affected')
(options, args) = argparser.parse_args()
upfile = None
try:
@@ -66,9 +71,15 @@ try:
if args[1] == 'update':
upfile = args[2]
else:
if args[1] == 'list':
comps = args[2:]
else:
comps = args[1:]
components = []
for arg in args[1:]:
for arg in comps:
components += arg.split(',')
if not components:
components = ['all']
except IndexError:
argparser.print_help()
@@ -88,6 +99,7 @@ def get_update_progress(session, url):
def update_firmware(session, filename):
global exitcode
session.stop_if_noderange_over(noderange, options.maxnodes)
output = sq.ScreenPrinter(noderange, session)
nodeurls = {}
filename = os.path.abspath(filename)
@@ -144,7 +156,7 @@ def show_firmware(session):
for prefix in inv:
firmware_shown = True
printfirm(node, prefix, inv[prefix])
if not firmware_shown:
if not firmware_shown and not exitcode:
argparser.print_help()
@@ -156,4 +168,4 @@ try:
update_firmware(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(exitcode)
+22 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -17,6 +17,7 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -35,9 +36,9 @@ import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog [options] \
\n %prog [options] nodegroup [list of attributes] \
\n %prog [options] nodegroup [list of attributes|all] \
\n %prog [options] nodegroup nodes=value1,value2 \
\n %prog -e nodegroup <attribute names to set> \
\n %prog -e nodegroup <attribute names to set> \
\n %prog [options] nodegroup nodes=value1,value2
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
@@ -47,6 +48,8 @@ argparser.add_option('-e', '--environment', action='store_true',
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear variables')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
@@ -72,7 +75,19 @@ if len(args) > 1:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
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
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options, argassign)
try:
# setting user output to what the user inputs
if args[1] == 'all':
@@ -82,7 +97,7 @@ if len(args) > 1:
requestargs=args[1:]
except Exception as e:
print str(e)
print(str(e))
if exitcode != 0:
sys.exit(exitcode)
@@ -108,6 +123,6 @@ else:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
sys.exit(exitcode)
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -47,7 +47,7 @@ session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
@@ -55,4 +55,4 @@ for r in session.create('/nodegroups/', attribs):
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
sys.exit(exitcode)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 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.
__author__ = 'jjohnson2,alin37,andywray'
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog\n")
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
nodelist = '/nodegroups/'
session = client.Command()
exitcode = 0
showtype='all'
requestargs=args[1:]
nodetype='noderange'
if len(args) > 0:
argparser.print_help()
sys.exit(1)
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)
if __name__ == '__main__':
main()
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -49,4 +49,4 @@ for r in session.delete('/nodegroups/{0}'.format(noderange)):
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
sys.exit(exitcode)
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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.
__author__ = 'jjohnson2,alin37,andywray'
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog <group> <new group name>\n")
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
nodelist = '/nodegroups/'
session = client.Command()
exitcode = 0
requestargs=args[1:]
nodetype='noderange'
if len(args) != 2:
argparser.print_help()
sys.exit(1)
else:
for res in session.update(
'/nodegroups/{0}/attributes/rename'.format(args[0]),
{'rename': args[1]}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print('{0}: {1}'.format(res['oldname'], res['newname']))
sys.exit(exitcode)
if __name__ == '__main__':
main()
+14 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,7 +32,8 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
@@ -44,6 +45,13 @@ except IndexError:
client.check_globbing(noderange)
codemappings = {
'ok': 0,
'warning': 1,
'critical': 2,
'failed': 2,
}
def main():
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
session = client.Command()
@@ -66,9 +74,11 @@ def main():
if 'error' in health[node]:
sys.stderr.write('{0}: Error: {1}\n'.format(
node, health[node]['error']))
exitcode |= 1
exitcode = 3
if 'health' in health[node]:
healthbynode[node] = health[node]['health']['value']
if codemappings[healthbynode[node]] > exitcode:
exitcode = codemappings[healthbynode[node]]
if 'sensors' in health[node]:
healthexplanations[node] = []
for sensor in health[node]['sensors']:
@@ -94,5 +104,5 @@ try:
main()
except KeyboardInterrupt:
print('')
sys.exit(0)
sys.exit(exitcode)
+2 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -31,7 +31,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off|blink]")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -42,9 +42,6 @@ client.check_globbing(noderange)
identifystate = None
if len(sys.argv) > 2:
identifystate = sys.argv[2]
else:
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
+20 -10
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -34,7 +34,8 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
if sys.version_info[0] < 3:
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
@@ -50,15 +51,17 @@ def print_mem_info(node, prefix, meminfo):
memdescfmt = '{0}GB PC'
if meminfo['memory_type'] == 'DDR3 SDRAM':
memdescfmt += '3-{1} '
elif meminfo['memory_type'] == 'DDR4 SDRAM':
elif 'DDR4' in meminfo['memory_type']:
memdescfmt += '4-{1} '
else:
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
return
if meminfo['ecc']:
if meminfo.get('ecc', False):
memdescfmt += 'ECC '
capacity = meminfo['capacity_mb'] / 1024
memdescfmt += meminfo['module_type']
modtype = meminfo.get('module_type', None)
if modtype:
memdescfmt += modtype
memdesc = memdescfmt.format(capacity, meminfo['speed'])
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
print('{0}: {1} manufacturer: {2}'.format(
@@ -66,10 +69,11 @@ def print_mem_info(node, prefix, meminfo):
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
print('{0}: {1} serial number: {2}'.format(node, prefix,
meminfo['serial']))
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
if 'manufacture_date' in meminfo:
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
exitcode = 0
@@ -87,6 +91,7 @@ def printerror(res, node=None):
url = '/noderange/{0}/inventory/hardware/all/all'
usedprefixes = set([])
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
@@ -129,6 +134,11 @@ try:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
idx = 2
while (node, prefix) in usedprefixes:
prefix = '{0} {1}'.format(inv['name'], idx)
idx += 1
usedprefixes.add((node, prefix))
if not inv['present']:
if not filters:
if options.json:
@@ -136,7 +146,7 @@ try:
databynode[node] = {}
databynode[node][prefix] = inv
else:
print '{0}: {1}: Not Present'.format(node, prefix)
print('{0}: {1}: Not Present'.format(node, prefix))
continue
info = inv['information']
info.pop('board_extra', None)
+151
View File
@@ -0,0 +1,151 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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 optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
exitcode = 0
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][install <file>|save <directory>|delete <name>]")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to delete licenses from, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
upfile = None
downdir = None
delete = False
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'install':
upfile = args[2]
elif args[1] == 'save':
downdir = args[2]
elif args[1] == 'delete':
delete = args[2]
elif args[1] != 'list':
argparser.print_help()
sys.exit(1)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def install_license(session, filename):
global exitcode
if not os.path.exists(filename):
sys.stderr.write('Unable to locate requested file {0}\n'.format(
filename))
sys.exit(404)
resource = '/noderange/{0}/configuration/' \
'management_controller/licenses/'.format(noderange)
filename = os.path.abspath(filename)
instargs = {'filename': filename}
for res in session.create(resource, instargs):
for node in res.get('databynode', []):
if 'error' in res['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(
node, res['databynode'][node]['error']))
sys.exit(res['databynode'][node].get('errorcode', 1))
show_licenses(session)
def save_licenses(session, dirname):
global exitcode
resource = '/noderange/{0}/configuration/' \
'management_controller/save_licenses'.format(noderange)
filename = os.path.abspath(dirname)
if not os.path.exists(filename):
sys.stderr.write('Unable to locate specified directory {0}\n'.format(
filename))
sys.exit(404)
instargs = {'dirname': filename}
for res in session.create(resource, instargs):
for node in res.get('databynode', {}):
fname = res['databynode'][node].get('filename', None)
if fname:
print('{0}: Saved license to {1}'.format(node, fname))
else:
sys.stderr.write('{0}: {1}', node, repr(res['databynode'][node]))
def show_licenses(session):
global exitcode
for res in session.read(
'/noderange/{0}/configuration/management_controller/licenses/'
'all'.format(noderange)):
for node in res.get('databynode', {}):
for license in res['databynode'][node].get('License', []):
msg = '{0}: {1}'.format(node, license.get('feature',
'Unknown'))
if license.get('state', 'Active') != 'Active':
msg += ' ({0})'.format(license['state'])
print(msg)
def delete_license(session, licname):
global exitcode
licstodel = []
for res in list(session.read(
'/noderange/{0}/configuration/management_controller/licenses/'
'all'.format(noderange))):
for node in res.get('databynode', {}):
for license in res['databynode'][node].get('License', []):
if license.get('feature', None) == licname:
prefix = '/nodes/{0}/configuration/management_controller/licenses/'.format(node)
for currlic in list(session.read(prefix)):
currlic = currlic.get('item', {}).get('href', 'all')
if currlic == 'all':
continue
currname = list(session.read(prefix + currlic))[0]
currname = currname.get('License', [{}])[0].get('feature', None)
if currname == licname:
list(session.delete(prefix + currlic))
show_licenses(session)
try:
session = client.Command()
if upfile:
install_license(session, upfile)
elif downdir:
save_licenses(session, downdir)
elif delete:
session.stop_if_noderange_over(noderange, options.maxnodes)
delete_license(session, delete)
else:
show_licenses(session)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -61,7 +61,7 @@ def main():
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
+4 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -83,7 +83,8 @@ def list_media(noderange, media):
for node in res.get('databynode', []):
url = res['databynode'][node].get('url', None)
name = res['databynode'][node].get('name', None)
if url and not res['databynode'][node].get('secure', False):
if (url and not url.startswith('file:') and
not res['databynode'][node].get('secure', False)):
name += ' (insecure)'
if not name:
continue
@@ -187,4 +188,4 @@ def main():
sys.exit(1)
handler(noderange, media)
if __name__ == '__main__':
main()
main()
+7 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -37,6 +37,11 @@ argparser = optparse.OptionParser(
argparser.add_option('-p', '--showprevious', dest='previous',
action='store_true', default=False,
help='Show previous power state')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to change power state, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -72,4 +77,4 @@ if options.previous:
# add dictionary to session
session.add_precede_dict(prev)
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate))
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate, promptover=options.maxnodes))
+7 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -35,6 +35,10 @@ import confluent.client as client
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()
@@ -43,10 +47,11 @@ 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)
sys.exit(exitcode)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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 optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> <newname>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
identifystate = None
if len(sys.argv) > 2:
newname = sys.argv[2]
else:
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
session.simple_noderange_command(noderange, 'attributes/rename', newname))
+7 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -32,6 +32,10 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to reseat, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
@@ -43,7 +47,7 @@ session = client.Command()
exitcode = 0
errorNodes = set([])
session.stop_if_noderange_over(noderange, options.maxnodes)
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
@@ -56,7 +60,7 @@ for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": Reseat successful"
print(node + ": Reseat successful")
sys.exit(success)
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 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.
from collections import deque
import optparse
import os
import select
import shlex
import signal
import subprocess
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
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.screensqueeze as sq
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog location noderange:location",
)
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run rsync to, '
'prompting if over the threshold')
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of nodes to concurrently rsync')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2 or ':' not in args[-1]:
argparser.print_help()
sys.exit(1)
concurrentprocs = options.count
noderange, targpath = args[-1].split(':', 1)
client.check_globbing(noderange)
c = client.Command()
cmdstr = ' '.join(args[:-1])
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
cmdstr += ' {node}:' + targpath
currprocs = 0
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(noderange, options.maxnodes)
for exp in c.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value']
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
cmd = cmd.encode('utf-8')
cmdv = shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
rdy, _, _ = select.select(all, [], [], 10)
nodeerrs = {}
pernodeout = {}
pernodefile = {}
output = sq.ScreenPrinter(noderange, c)
while all:
for r in rdy:
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0)[0]:
data = r.read(1)
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = ''
pernodeout[node] += client.stringify(data)
if '\n' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
if currout:
pernodefile[node] = os.path.basename(currout)
if '\r' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\r', 1)
if currout:
currout = currout.split()
try:
currout = currout[1]
output.set_output(node, '{0}:{1}'.format(pernodefile[node], currout))
except IndexError:
pernodefile = currout[0]
pass
else:
output.set_output(node, 'error!')
if node not in nodeerrs:
nodeerrs[node] = ''
nodeerrs[node] += client.stringify(data)
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if node not in nodeerrs:
output.set_output(node, 'complete')
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
if all:
rdy, _, _ = select.select(all, [], [], 10)
for node in nodeerrs:
for line in nodeerrs[node].split('\n'):
sys.stderr.write('{0}: {1}\n'.format(node, line))
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, pipedesc):
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
'type': 'stdout'}
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
'type': 'stderr'}
all.add(nopen.stdout)
all.add(nopen.stderr)
if __name__ == '__main__':
run()
+23 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -39,11 +39,17 @@ import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange commandexpression",
usage="Usage: %prog [options] noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run the command with, '
'prompting if over the threshold')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
@@ -61,7 +67,7 @@ def run():
pipedesc = {}
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
@@ -69,7 +75,9 @@ def run():
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmd = ex[node]['value']
if not isinstance(cmd, bytes) and not isinstance(cmd, str):
cmd = cmd.encode('utf-8')
cmdv = shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
@@ -85,7 +93,7 @@ def run():
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
@@ -93,7 +101,11 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
@@ -107,7 +119,11 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
+19 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
@@ -114,7 +114,8 @@ def sensorpass(showout=True, appendtime=False):
continue
for redundant_state in ('Non-Critical', 'Critical'):
try:
sensedata['states'].remove(redundant_state)
if sensedata.get('states', False):
sensedata['states'].remove(redundant_state)
except ValueError:
pass
resultdata[node][sensedata['name']] = sensedata
@@ -132,11 +133,12 @@ def sensorpass(showout=True, appendtime=False):
showval = u' {0} '.format(sensedata['value'])
if sensedata['units'] not in (None, u''):
showval += sensedata['units']
if sensedata['health'] != 'ok':
if sensedata.get('health', 'ok') != 'ok':
datadescription = [sensedata['health']]
else:
datadescription = []
datadescription.extend(sensedata['states'])
if sensedata.get('states', False):
datadescription.extend(sensedata['states'])
if datadescription:
if showval == '':
showval += u' {0}'.format(
@@ -147,8 +149,11 @@ def sensorpass(showout=True, appendtime=False):
if appendtime:
showval += ' @' + time.strftime(
'%Y-%m-%dT%H:%M:%S')
print(u'{0}: {1}:{2}'.format(
node, sensedata['name'], showval).encode('utf-8'))
printval = u'{0}: {1}:{2}'.format(
node, sensedata['name'], showval)
if not isinstance(printval, str):
printval = printval.encode('utf-8')
print(printval)
sys.stdout.flush()
return resultdata
@@ -156,7 +161,7 @@ def sensorpass(showout=True, appendtime=False):
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
for nodekey in resdata:
if showtime:
if showtime.is_integer():
if isinstance(showtime, int):
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
else:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
@@ -183,6 +188,7 @@ def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
except KeyError:
rowdata.append('N/A')
csvwriter.writerow(rowdata)
sys.stdout.flush()
def main():
@@ -196,7 +202,11 @@ def main():
orderedsensors.append(name)
orderedsensors.sort()
for name in orderedsensors:
headernames.append(sensorheaders[name].encode('utf-8'))
headername = sensorheaders[name]
if (not isinstance(headername, str) and
not isinstance(headername, bytes)):
headername = headername.encode('utf-8')
headernames.append(headername)
if options.csv:
linebyline = False
csvwriter = csv.writer(sys.stdout)
@@ -220,7 +230,7 @@ def main():
sys.exit(exitcode)
sleeptime = nextstart - os.times()[4]
if sleeptime > 0:
time.sleep(nextstart - os.times()[4])
time.sleep(sleeptime)
else:
if options.csv:
format_csv(csvwriter, orderedsensors, resdata, showtime=False)
+9 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -43,7 +43,10 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true',
argparser.add_option('-u', '--uefi', dest='uefi', action='store_true',
default=True,
help='Request UEFI style boot (rather than BIOS)')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to modify next boot device, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
try:
@@ -53,8 +56,8 @@ except IndexError:
argparser.print_help()
sys.exit(1)
bootdev = None
if len(sys.argv) > 2:
bootdev = sys.argv[2]
if len(args) > 1:
bootdev = args[1]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
@@ -63,6 +66,7 @@ if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
session.stop_if_noderange_over(noderange, options.maxnodes)
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
persistent=options.persist))
+23 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -39,11 +39,17 @@ import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange commandexpression",
usage="Usage: %prog [options] noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to run remote ssh command to, '
'prompting if over the threshold')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
@@ -53,7 +59,7 @@ def run():
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
c = client.Command()
cmdstr = " ".join(args[1:])
currprocs = 0
@@ -62,7 +68,7 @@ def run():
pendingexecs = deque()
exitcode = 0
c.stop_if_noderange_over(args[0], options.maxnodes)
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
@@ -70,7 +76,9 @@ def run():
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmd = ex[node]['value']
if not isinstance(cmd, str) and not isinstance(cmd, bytes):
cmd = cmd.encode('utf-8')
cmdv = ['ssh', node, cmd]
if currprocs < concurrentprocs:
currprocs += 1
@@ -94,7 +102,11 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
@@ -108,7 +120,11 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 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 optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
exitcode = 0
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def mbtohuman(mb):
if mb > 1000000:
return '{0:.3f} TB'.format(mb/1000000.0)
if mb > 1000:
return '{0:.3f} GB'.format(mb/1000.0)
return '{0:.3f} MB'.format(mb)
def showstorage(noderange, options, args):
global exitcode
session = client.Command()
disks = {}
arrays = {}
volumes = {}
scfg = session.read('/noderange/{0}/configuration/storage/all'.format(
noderange))
_print_cfg(scfg)
def _print_cfg(scfg):
global exitcode
storagebynode = {}
for e in scfg:
if 'error' in e:
sys.stderr.write(e['error'] + '\n')
exitcode = e.get('errorcode', 1)
for node in e.get('databynode', {}):
curr = e['databynode'][node]
if 'error' in curr:
if 'no available drives' in curr['error']:
curr['error'] += ' (drives must be in unconfigured state to be available, they must not be in jbod or online state)'
sys.stderr.write('{0}: {1}\n'.format(node, curr['error']))
exitcode = curr.get('errorcode', 1)
continue
if node not in storagebynode:
storagebynode[node] = {'disks': [], 'arrays': [],
'volumes': []}
storagebynode[node][curr['type'] + 's'].append(curr)
for node in storagebynode:
for disk in sorted(storagebynode[node]['disks'],
key=lambda x: x['name']):
print('{0}: Disk {1} Description: {2}'.format(
node, disk['name'], disk['description']))
print('{0}: Disk {1} State: {2}'.format(node, disk['name'],
disk['state']))
print('{0}: Disk {1} FRU: {2}'.format(node, disk['name'],
disk['fru']))
print('{0}: Disk {1} Serial Number: {2}'.format(node, disk['name'],
disk['serial']))
if disk['array']:
print('{0}: Disk {1} Array: {2}'.format(node, disk['name'],
disk['array']))
for arr in storagebynode[node]['arrays']:
print('{0}: Array {1} Available Capacity: {2}'.format(
node, arr['id'], mbtohuman(arr['available'])))
print('{0}: Array {1} Total Capacity: {2}'.format(
node, arr['id'], mbtohuman(arr['capacity'])))
print('{0}: Array {1} RAID: {2}'.format(node, arr['id'],
arr['raid']))
print('{0}: Array {1} Disks: {2}'.format(node, arr['id'], ','.join(
arr['disks'])))
print(
'{0}: Array {1} Volumes: {2}'.format(node, arr['id'], ','.join(
arr['volumes'])))
for vol in storagebynode[node]['volumes']:
print('{0}: Volume {1}: Size: {2}'.format(node, vol['name'],
mbtohuman(vol['size'])))
print('{0}: Volume {1}: State: {2}'.format(node, vol['name'],
vol['state']))
print('{0}: Volume {1}: Array {2}'.format(node, vol['name'],
vol['array']))
def createstorage(noderange, options, args):
if options.raidlevel is None or options.disks is None:
sys.stderr.write('-r and -d are required arguments to create array\n')
sys.exit(1)
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
names = options.name
if names is None:
names = ''.join(args)
parms = {'disks': options.disks, 'raidlevel': options.raidlevel,
'name': names}
if options.size:
parms['size'] = options.size
if options.stripsizes:
parms['stripsizes'] = options.stripsizes
_print_cfg(session.create(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names), parms))
def deletestorage(noderange, options, args):
if options.name is None:
if len(args) == 1:
names = args[0]
else:
sys.stderr.write('-n is required to indicate volume(s) to delete\n')
sys.exit(1)
else:
names = options.name
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
for rsp in session.delete(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names)):
if 'deleted' in rsp:
print('Deleted: {0}'.format(rsp['deleted']))
elif 'databynode' in rsp:
for node in rsp['databynode']:
if 'error' in rsp['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(
node, rsp['databynode'][node]['error']))
else:
sys.stderr.write('{0}: {1}\n'.format(
node, repr(rsp['databynode'][node])))
else:
print(repr(rsp))
def setdisk(noderange, options, args):
if options.disks is None:
if len(args):
names = args.pop(0)
else:
sys.stderr.write('-d is required to indicate disk to modify\n')
sys.exit(1)
else:
names = options.disks
if not len(args) or args[0] not in ('hotspare', 'jbod', 'unconfigured'):
sys.stderr.write('diskset requires valid state as argument (hotspare, jbod, unconfigured)\n')
sys.exit(1)
session = client.Command()
session.stop_if_noderange_over(noderange, options.maxnodes)
scfg = session.update('/noderange/{0}/configuration/storage/disks/{1}'.format(noderange, names), {'state': args[0]})
_print_cfg(scfg)
funmap = {
'create': createstorage,
'show': showstorage,
'diskset': setdisk,
'delete': deletestorage,
'rm': deletestorage,
}
def main():
argparser = OptParser(
usage='Usage: %prog <noderange> [show|create|delete|diskset]',
epilog='',
)
argparser.add_option('-r', '--raidlevel', type='int',
help='RAID level to use when creating an array')
argparser.add_option('-d', '--disks', type='str',
help='Comma separated list of disks to use, or the '
'word "rest" to indicate use of all available '
'disks')
argparser.add_option('-s', '--size', type='str',
help='Comma separated list of sizes to use when '
'creating volumes. The sizes may be absolute '
'size (e.g. 16gb), percentage (10%) or the word '
'"rest" to use remaining capacity, default '
'behavior is to use all capacity to make a '
'volume')
argparser.add_option('-n', '--name', type='str',
help='Comma separated list of names to use when '
'naming volumes, or selecting a volume for '
'delete. Default behavior is to use '
'implementation provided default names.')
argparser.add_option('-z', '--stripsizes', type='str',
help='Comma separated list of stripsizes to use when creating volumes. '
'This value is in kilobytes. The default behavior is to allow the '
'storage controller to decide.')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to configure storage on, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
if len(args) == 1:
args.append('show')
try:
noderange = args[0]
operation = args[1]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
try:
handler = funmap[operation]
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, options, args[2:])
if __name__ == '__main__':
main()
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 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 optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
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.screensqueeze as sq
exitcode = 0
def get_update_progress(session, url):
for res in session.read(url):
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
text = '{0}: {1:3.0f}%'.format(status, percent)
return text, status, detail
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
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
for node in res.get('databynode', ()):
printerror(res['databynode'][node], node)
def download_servicedata(noderange, media, options):
global exitcode
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/support/servicedata/'.format(noderange)
upargs = {'filename': filename}
noderrs = {}
nodeurls = {}
session.stop_if_noderange_over(noderange, options.maxnodes)
for res in session.create(resource, upargs):
if 'created' not in res:
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
'error', 'Unknown Error')
continue
watchurl = res['created']
currnode = watchurl.split('/')[1]
nodeurls[currnode] = '/' + watchurl
while nodeurls:
for node in list(nodeurls):
progress, status, err = get_update_progress(
session, nodeurls[node])
if status == 'error':
exitcode = 1
noderrs[node] = err
if status in ('error', 'complete', 'pending'):
list(session.delete(nodeurls[node]))
del nodeurls[node]
output.set_output(node, progress)
time.sleep(2)
allerrnodes = ','.join(noderrs)
if noderrs:
sys.stderr.write(
'Nodes had errors retrieving service data ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
funmap = {
'servicedata': download_servicedata,
}
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> servicedata "
"<filename>",
epilog='\nservicedata will save service data to the given '
'directory. It is saved to the location on the relevant '
'management server (the confluent server if running remote, '
'and the collective.manager if in collective)\n'
'\n\nSee `man %prog` for more info.\n')
argparser.add_option('-m', '--maxnodes', type='int',
help='Specify a maximum number of '
'nodes to download diagnostic data from, '
'prompting if over the threshold')
(options, args) = argparser.parse_args()
media = None
try:
noderange = args[0]
operation = args[1]
arglength = 2
if operation == 'servicedata':
media = args[2]
arglength = 3
if len(args) > arglength:
argparser.print_help()
sys.exit(1)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
try:
handler = funmap[operation]
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media, options)
if __name__ == '__main__':
main()
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 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 argparse
import csv
import fcntl
import io
import numpy as np
import os
import sixel
import subprocess
import sys
class DumbWriter(sixel.SixelWriter):
def restore_position(self, output):
return
def plot(gui, output, plotdata, bins):
import matplotlib as mpl
if not gui:
mpl.use('Agg')
import matplotlib.pyplot as plt
n, bins, patches = plt.hist(plotdata, bins)
plt.show()
if not gui:
if output:
tdata = output
else:
tdata = io.BytesIO()
plt.savefig(tdata)
if not gui and not output:
writer = DumbWriter()
writer.draw(tdata)
return n, bins
def textplot(plotdata, bins):
n, bins = np.histogram(plotdata, bins)
labels = []
for bin in bins:
labels.append('{0:0.1f}'.format(bin))
width = 80
# Since this will be primarily piped into, hard to get
# terminal width
labelwidth = 0
for lab in labels:
if len(lab) > labelwidth:
labelwidth = len(lab)
width -= (labelwidth) + 1
labelfmt = '{{0:>{0}s}}|'.format(labelwidth)
maxn = 0.0
for lgth in n:
if lgth > maxn:
maxn = float(lgth)
for i in range(len(n)):
print(labelfmt.format(labels[i]) + '=' * int(np.round((n[i]/maxn) * width)))
return n, bins
histogram = False
aparser = argparse.ArgumentParser(description='Quick access to common statistics')
aparser.add_argument('-c', type=int, default=0, help='Column number to analyze (default is last column)')
aparser.add_argument('-d', default=None, help='Value used to separate columns')
aparser.add_argument('-x', default=False, action='store_true', help='Output histogram in sixel format')
aparser.add_argument('-s', default=0, help='Number of header lines to skip before processing')
aparser.add_argument('-g', default=False, action='store_true', help='Open histogram in separate graphical window')
aparser.add_argument('-o', default=None, help='Output histogram to the specified filename in PNG format')
aparser.add_argument('-t', default=False, action='store_true', help='Output a histogram in text format')
aparser.add_argument('-v', default=False, action='store_true', help='Attempt to list nodes relevant to each histogram bar (requires -s, -o, or -t)')
aparser.add_argument('-b', type=int, default=10, help='Number of bins to use in histogram (default is 10)')
args = aparser.parse_args(sys.argv[1:])
plotdata = []
headlines = int(args.s)
while headlines >= 0:
data = sys.stdin.readline()
headlines -= 1
if args.d:
delimiter = args.d
else:
if '\t' in data:
delimiter = '\t'
elif ' ' in data:
delimiter = ' '
elif ',' in data:
delimiter = ','
else:
delimiter = ' ' # handle single column
data = list(csv.reader([data], delimiter=delimiter))[0]
nodebydatum = {}
idx = args.c - 1
autoidx = False
while data:
node = None
if ':' in data[0]:
node, data[0] = data[0].split(':', 1)
else:
node = data[0]
if idx == -1 and not autoidx:
while not autoidx:
try:
datum = float(data[idx])
except ValueError:
idx -= 1
continue
except IndexError:
sys.stderr.write('Unable to identify a numerical column\n')
sys.exit(1)
autoidx = True
else:
datum = float(data[idx])
if node:
if datum in nodebydatum:
nodebydatum[datum].add(node)
else:
nodebydatum[datum] = set([node])
plotdata.append(datum)
data = sys.stdin.readline()
data = list(csv.reader([data], delimiter=delimiter))[0]
n = None
if args.g or args.o or args.x:
n, bins = plot(args.g, args.o, plotdata, bins=args.b)
if args.t:
n, bins = textplot(plotdata, bins=args.b)
print('Samples: {5} Min: {3} Median: {0} Mean: {1} Max: {4} StandardDeviation: {2} Sum: {6}'.format(np.median(plotdata), np.mean(plotdata), np.std(plotdata), np.min(plotdata), np.max(plotdata), len(plotdata), np.sum(plotdata)))
if args.v and n is not None and nodebydatum:
print('')
currbin = bins[0]
bins = bins[1:]
currbinmembers = []
for datum in sorted(nodebydatum):
if datum > bins[0]:
nextbin = None
endbin = bins[0]
while len(bins) and bins[0] < datum:
nextbin = bins[0]
bins = bins[1:]
if not nextbin:
nextbin = np.max(plotdata)
print('Entries between {0} and {1}'.format(currbin, endbin))
currbin = nextbin
print('-' * 80)
print(','.join(sorted(currbinmembers)))
print('')
print('')
currbinmembers = []
for node in nodebydatum[datum]:
currbinmembers.append(node)
if currbinmembers:
print('Entries between {0} and {1}'.format(currbin, np.max(plotdata)))
print('-' * 80)
print(','.join(sorted(currbinmembers)))
print('')
print('')
+1
View File
@@ -0,0 +1 @@
../confluent_server/builddeb
+142 -19
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2018 Lenovo
# 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.
@@ -15,8 +15,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import anydbm as dbm
try:
import anydbm as dbm
except ImportError:
import dbm
import csv
import errno
import fnmatch
import hashlib
import os
import shlex
@@ -24,6 +29,7 @@ import socket
import ssl
import sys
import confluent.tlvdata as tlvdata
import confluent.sortutil as sortutil
SO_PASSCRED = 16
@@ -33,6 +39,71 @@ _attraliases = {
'bmcpass': 'secret.hardwaremanagementpassword',
}
try:
input = raw_input
except NameError:
pass
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')
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
@@ -49,7 +120,10 @@ def printerror(res, node=None):
def cprint(txt):
print(txt)
try:
print(txt)
except UnicodeEncodeError:
print(txt.encode('utf8'))
sys.stdout.flush()
def _parseserver(string):
@@ -81,6 +155,8 @@ class Command(object):
self.serverloc = server
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
self._connect_tls()
tlvdata.recv(self.connection)
@@ -147,7 +223,7 @@ class Command(object):
return rc
def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, **kwargs):
key=None, errnodes=None, promptover=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
@@ -163,6 +239,7 @@ class Command(object):
noderange, resource)):
rc = self.handle_results(ikey, rc, res, errnodes)
else:
self.stop_if_noderange_over(noderange, promptover)
kwargs[ikey] = input
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
@@ -172,6 +249,33 @@ class Command(object):
except KeyboardInterrupt:
cprint('')
return 0
def stop_if_noderange_over(self, noderange, maxnodes):
if maxnodes is None:
return
nsize = self.get_noderange_size(noderange)
if nsize > maxnodes:
if nsize == 1:
nodename = list(self.read(
'/noderange/{0}/nodes/'.format(noderange)))[0].get('item', {}).get('href', None)
nodename = nodename[:-1]
p = input('Command is about to affect node {0}, continue (y/n)? '.format(nodename))
else:
p = input('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")
def get_noderange_size(self, noderange):
numnodes = 0
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
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
@@ -265,13 +369,14 @@ class Command(object):
if knownhosts:
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 = raw_input(
replace = input(
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
@@ -311,7 +416,7 @@ def attrrequested(attr, attrlist, seenattributes):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if candidate.lower() == attr.lower():
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
seenattributes.add(truename)
return True
elif attr.lower().startswith(candidate.lower() + '.'):
@@ -324,8 +429,12 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return 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 k[1]['sortid']
return k[0]
def print_attrib_path(path, session, requestargs, options, rename=None):
def print_attrib_path(path, session, requestargs, options, rename=None, attrprefix=None):
exitcode = 0
seenattributes = set([])
for res in session.read(path):
@@ -334,9 +443,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
exitcode = 1
continue
for node in sorted(res['databynode']):
for attr, val in sorted(
res['databynode'][node].items(),
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
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
@@ -348,6 +455,8 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
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):
if 'value' in currattr:
@@ -407,9 +516,12 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
(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, currattr['value'],
currattr['default']))
node, printattr, cval, dval))
else:
try:
@@ -418,11 +530,14 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
details = False
if details:
if currattr.get('help', None):
attrout += ' (Help: {0})'.format(
currattr['help'].encode('utf-8'))
attrout += u' (Help: {0})'.format(
currattr['help'])
if currattr.get('possible', None):
attrout += ' (Choices: {0})'.format(
','.join(currattr['possible']))
try:
attrout += u' (Choices: {0})'.format(
','.join(currattr['possible']))
except TypeError:
pass
cprint(attrout)
if not exitcode:
if requestargs:
@@ -493,7 +608,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
exitcode = 1
return exitcode
def updateattrib(session, updateargs, nodetype, noderange, options):
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
# update attribute
exitcode = 0
if options.clear:
@@ -526,11 +641,19 @@ def updateattrib(session, updateargs, nodetype, noderange, options):
'attributes/all',
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
try:
for val in updateargs[1:]:
val = val.split('=')
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
@@ -584,4 +707,4 @@ def check_globbing(noderange):
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
sys.exit(1)
@@ -44,6 +44,8 @@ class ScreenPrinter(object):
self.fieldwidth = maxlen + textlen + 1 # 1 for column
def set_output(self, node, text):
if self.nodeoutput[node] == text:
return
self.nodeoutput[node] = text
if len(text) >= self.textlen:
self.textlen = len(text) + 1
+7 -2
View File
@@ -169,13 +169,18 @@ class GroupedData(object):
output.flush()
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
count=False, basenode=None):
self.generate_byoutput()
modaloutput = None
ismodal = True
revoutput = []
if basenode:
for checkout in self.byoutput:
if basenode in self.byoutput[checkout]:
modaloutput = checkout
for outdata in sorted(
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
self.byoutput, key=lambda x: [0 if modaloutput == x else 1,
0 - len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
+10 -3
View File
@@ -25,6 +25,11 @@ try:
except NameError:
unicode = str
try:
range = xrange
except NameError:
pass
def decodestr(value):
ret = None
try:
@@ -34,11 +39,13 @@ def decodestr(value):
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], str):
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')
@@ -49,7 +56,7 @@ def unicode_dictvalues(dictdata):
def _unicode_list(currlist):
for i in xrange(len(currlist)):
for i in range(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
@@ -64,7 +71,7 @@ def send(handle, data):
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, str) or isinstance(data, unicode):
if isinstance(data, bytes) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
+10 -2
View File
@@ -24,10 +24,18 @@ a confluent server.
%setup -n %{name}-%{version} -n %{name}-%{version}
%build
python setup.py build
%if "%{dist}" == ".el8"
python3 setup.py build
%else
python2 setup.py build
%endif
%install
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%if "%{dist}" == ".el8"
python3 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%else
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-scripts=/opt/confluent/bin --install-purelib=/opt/confluent/lib/python
%endif
%clean
+246
View File
@@ -36,4 +36,250 @@ alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export C
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
alias nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
alias nodelicense='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelicense'
# Do not continue for non-bash shells, the rest of this sets up bash completion functions
[ -z "$BASH_VERSION" -o -z "$PS1" ] && return
_confluent_get_args()
{
CMPARGS=($COMP_LINE)
NUMARGS=$((COMP_CWORD+1))
if [ "${COMP_WORDS[COMP_CWORD]}" == '' ]; then
CMPARGS+=("")
fi
GENNED=""
for CAND in ${COMP_CANDIDATES[@]}; do
candarray=(${CAND//,/ })
matched=0
for c in "${candarray[@]}"; do
for arg in "${CMPARGS[@]}"; do
if [ "$arg" = "$c" ]; then
matched=1
break
fi
done
done
if [ 0 = $matched ]; then
for c in "${candarray[@]}"; do
GENNED+=" $c"
done
fi
done
}
function _confluent_generic_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeidentify_completion()
{
COMP_CANDIDATES=("on,off,blink -h")
_confluent_generic_completion
}
_confluent_nodesetboot_completion()
{
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
_confluent_generic_completion
}
_confluent_nodepower_completion()
{
COMP_CANDIDATES=("boot,off,on,status -h -p")
_confluent_generic_completion
}
_confluent_nodemedia_completion()
{
COMP_CANDIDATES=("list,upload,attach,detachall -h")
_confluent_get_args
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[-2]} == 'upload' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodefirmware_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeshell_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -c -- ${COMP_WORDS[COMP_CWORD]}))
return
fi
if [ $NUMARGS -gt 3 ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodelicense_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "install list save delete" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'save' ]; then
compopt -o dirnames
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
}
_confluent_nodesupport_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
compopt -o dirnames
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
}
_confluent_nodeattrib_completion()
{
COMP_CANDIDATES=$(nodeattrib '~.>1' all | awk '{print $2}'|sed -e 's/://')
_confluent_generic_completion
}
_confluent_nn_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
}
_confluent_nr_completion()
{
CMPARGS=($COMP_LINE)
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
}
_confluent_ng_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[COMP_CWORD]}" ]; then
PREFIX=${COMP_WORDS[COMP_CWORD]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
}
complete -F _confluent_nodeattrib_completion nodeattrib
complete -F _confluent_nodeattrib_completion nodegroupattrib
complete -F _confluent_nr_completion nodebmcreset
complete -F _confluent_nodesetboot_completion nodeboot
complete -F _confluent_nr_completion nodeconfig
complete -F _confluent_nn_completion nodeconsole
complete -F _confluent_nr_completion nodeeventlog
complete -F _confluent_nodefirmware_completion nodefirmware
complete -F _confluent_ng_completion nodegroupattrib
complete -F _confluent_ng_completion nodegroupremove
complete -F _confluent_nr_completion nodehealth
complete -F _confluent_nodeidentify_completion nodeidentify
complete -F _confluent_nr_completion nodeinventory
complete -F _confluent_nodeattrib_completion nodelist
complete -F _confluent_nodemedia_completion nodemedia
complete -F _confluent_nodepower_completion nodepower
complete -F _confluent_nr_completion noderemove
complete -F _confluent_nr_completion nodereseat
complete -F _confluent_nodeshell_completion noderun
complete -F _confluent_nr_completion nodesensors
complete -F _confluent_nodesetboot_completion nodesetboot
complete -F _confluent_nodeshell_completion nodeshell
complete -F _confluent_nodesupport_completion nodesupport
complete -F _confluent_nodelicense_completion nodelicense
+6 -2
View File
@@ -3,7 +3,7 @@ collate(1) -- Organize text input by node
## SYNOPSIS
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
## DESCRIPTION
@@ -26,6 +26,10 @@ node and group names sorted alphanumerically.
Express all but the most common result group in terms of diff from
the most common result group
* `-l`, `--log`:
Save output per node to individual log files, replacing {node} in the name
with the nodename of each
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
@@ -85,4 +89,4 @@ node and group names sorted alphanumerically.
` Processors.ExecuteDisableBit=Enable`
+55
View File
@@ -0,0 +1,55 @@
collective(1) -- Check and manage a confluent collective
==============================
## SYNOPSIS
`collective invite <server>`
`collective join <server> [-i TOKEN]`
`collective show`
`collective gencert`
## DESCRIPTION
**collective** helps manage the collective mode of confluent, where multiple
confluent servers are linked together to act as one. For example, the procedure to set up
a collective to run on three servers called mgt1, mgt2, and mgt3, first install and start
confluent as usual on the three servers. On mgt1, run `collective invite mgt2` and an
invitation token will be output. On mgt2, either run `collective join mgt1` to paste
the token interactively, or `collective join mgt1 -i <token>`. At this point, either
mgt1 or mgt2 can bring in mgt3. For example on mgt2 run `collective invite mgt3` and
on mgt3 run `collective join mgt2 -i <token>`
This can be linked together in the following manner with ssh:
on mgt1:
`# ssh mgt2 collective join mgt1 -i $(collective invite mgt2)`
Note that a collective is only redundant with 3 or more members. The collective
will function so long as more than half of the members are online. A collective
of two members is supported, but without redundancy.
Also note that the collective leader role is dynamic, but has no impact on interacting
with confluent. It is merely an internal role that can dynamically change depending
on circumstances.
## OPTIONS
* `-i`:
Provide the token as an argument rather than interactively.
## EXAMPLES
* Inviting a server called mgt2:
`# collective invite mgt2`
`bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
* On mgt2, joining mgt1:
`# collective join mgt1 -i bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
`Success`
* Showing the collective state:
`# collective show`
`Quorum: True`
`Leader: mgt1`
`Active collective members:`
` mgt2`
+12
View File
@@ -7,6 +7,7 @@ nodeattrib(8) -- List or change confluent nodes attributes
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -22,6 +23,9 @@ 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.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
If the word all is specified, then all available attributes are given.
Omitting any attribute name or the word 'all' will display only attributes
that are currently set.
@@ -40,6 +44,8 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
Clear specified nodeattributes
* `-e`, `--environment`:
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
* `-p`, `--prompt`:
Request interactive prompting to provide values rather than the command line or environment variables.
## EXAMPLES
* Listing matching nodes of a simple noderange:
@@ -85,6 +91,12 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
`n1: console.method: `
`n2: console.method: `
* List all switches that a node is described as connected to:
`# nodeattrib d1 net.*switch`
`d1: net.mgt.switch: mgtswitch1`
`d1: net.pxe.switch: pxeswitch1`
`d1: net.switch:`
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
@@ -9,6 +9,18 @@ expression to generate the value.
An expression will contain some directives wrapped in `{}` characters. Within
`{}` are a number of potential substitute values and operations.
Note that syntax of expressions can have overlap with the shell syntax.
For example:
`$ echo (n2)`
`-bash: syntax error near unexpected token `n2'`
In such a case, it helps to quote the expression to allow it to be passed:
`$ echo '(n2)'`
`(n2)`
The most common operation is to extract a number from the nodename. These
values are available as n1, n2, etc. So for example attributes for a node named
b1o2r3u4 would have {n1} as 1, {n2} as 2, {n3} as 3, and {n4} as 4.
+34 -2
View File
@@ -4,12 +4,44 @@ nodeboot(8) -- Reboot a confluent node to a specific device
## SYNOPSIS
`nodeboot <noderange>`
`nodeboot <noderange>` [net|setup]
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
## DESCRIPTION
**nodeboot** reboots nodes in a noderange. If an additional argument is given,
it sets the node to specifically boot to that as the next boot.
it sets the node to specifically boot to that as the next boot. This
performs an immediate reboot without waiting for the OS. To set the boot
device without inducing a reboot, see the `nodesetboot` command.
## OPTIONS
* `-b`, `--bios`:
For a system that supports both BIOS and UEFI style boot, request BIOS style
boot if supported (some platforms will UEFI boot with this flag anyway).
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `default`:
Request a normal default boot with no particular device override
* `cd`:
Request boot from media. Note that this can include physical CD,
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
key devices.
* `network`:
Request boot to network
* `setup`:
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
* `hd`:
Boot straight to hard disk drive
## EXAMPLES
* Booting n3 and n4 to the default boot behavior:
+10 -2
View File
@@ -3,8 +3,8 @@ nodeconfig(8) -- Show or change node configuration
## SYNOPSIS
`nodeconfig <noderange> [options] [<configuration>..]`
`nodeconfig <noderange> [options] [<configuration=value>..]`
`nodeconfig <noderange> [options] [<configuration>..]`
`nodeconfig <noderange> [options] [<configuration=value>..]`
## DESCRIPTION
@@ -30,6 +30,14 @@ given as a node expression, as documented in the man page for nodeattribexpressi
Provide detailed data as available. This can include help text and valid
values for a setting.
* `-r`, `--restoredefault`:
Request that the specified component of the targeted nodes will have its
configuration reset to default. Currently the only component implemented
is uefi.
* `-b`, '--batch':
Provide arguments as lines of a file, rather than the command line.
## EXAMPLES
* Showing the current IP configuration of noderange BMC/IMM/XCC:
+7 -1
View File
@@ -2,7 +2,7 @@ nodeconsole(8) -- Open a console to a confluent node
=====================================================
## SYNOPSIS
`nodeconsole <node>`
`nodeconsole [options] <noderange>`
## DESCRIPTION
@@ -16,6 +16,12 @@ will initiate an automatic retry interval that is randomized between 2 and 4 min
The reopen escape sequence below requests an immediate retry, as does connecting
a new session.
## OPTIONS
* `-t`, `--tile`:
Use tmux to arrange consoles of the given noderange into a tiled layout on
the terminal screen
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
+1
View File
@@ -1,4 +1,5 @@
nodedefine(8) -- Define new confluent nodes
===================================================================
## SYNOPSIS
@@ -75,6 +75,20 @@ the nodes.
* `-e MAC`, `--ethaddr=MAC`:
Operate against the system with the specified MAC
address
* `-f FIELDS`, `--fields=FIELDS`:
Request a custom set of fields. The available fields are:
Node: The node name if a correlation has been identified
Model: The model number
Serial: The serial number
UUID: The UUID as it should appear in DMI
Type: Device type (e.g. lenovo-xcc, pxe-client, etc)
IP: The confirmed working IP addresses associated with the record
Mac: Mac address of the relevant network interface
Switch: The nearest detected switch to the entry
Port: Port of the switch that most closely connects to the network interface
Advertised IP: IP addresses that may not have been confirmed, but are advertised
* `-o ORDER`, `--order=ORDER`:
Order output by given field. Field names are the same as documented in the -f argument.
* `-t TYPE`, `--type=TYPE`:
Operate against the system of the specified type
* `-c`, `--csv`:
+6 -2
View File
@@ -4,7 +4,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
## SYNOPSIS
`nodefirmware <noderange>`
`nodefirmware <noderange> <components>|core`
`nodefirmware <noderange> list|<components>|core`
`nodefirmware <noderange> update [--backup] <filename>`
## DESCRIPTION
@@ -13,7 +13,11 @@ nodefirmware(8) -- Report firmware information on confluent nodes
will retrieve all firmware, but can be directed to fetch specific firmware by
calling out the name of the firmware (e.g. uefi or xcc) or request reading only
core firmware firmware by using the word 'core', which is generally a quicker
operation.
operation. Different hardwaremanagement.method indicated plugins may have
different capabilities available. For example, the 'core' distinction may
not be relevant to redfish. Additionally, the Lenovo XCC makes certain
information available over IPMI that is not otherwise available (for example
the FPGA version where applicable).
In the update form, it accepts a single file and attempts to update it using
the out of band facilities. Firmware updates can end in one of three states:
@@ -7,6 +7,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
`nodegroupattrib <group> [<nodeattribute>...]`
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -1,4 +1,5 @@
nodegroupdefine(8) -- Define new confluent node group
===================================================================
## SYNOPSIS
@@ -6,7 +7,7 @@ nodegroupdefine(8) -- Define new confluent node group
## DESCRIPTION
`nodegroupdefine` allows the definition of a new node for the confluent management
`nodegroupdefine` allows the definition of a new nodegroup for the confluent management
service. It may only define a single group name at a time.
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
that `nodegroupattrib(8)` will error if a node group does not exist.
@@ -0,0 +1,14 @@
nodegrouplist(8) -- List the defined confluent nodegroups
===================================================================
## SYNOPSIS
`nodegrouplist`
## DESCRIPTION
`nodegrouplist` lists the currently defined groups in confluent.
## SEE ALSO
nodeattrib(8), nodeattribexpressions(5), nodegroupattrib(8)
+2 -1
View File
@@ -3,7 +3,7 @@ nodeidentify(8) -- Control the identify LED of confluent nodes
## SYNOPSIS
`nodidentify <noderange> [on|off]`
`nodidentify <noderange> [on|off|blink]`
## DESCRIPTION
@@ -13,6 +13,7 @@ options are supported:
* `on`: Turn on the identify LED
* `off`: Turn off the identify LED
* `blink`: Set the identify LED to blink (when supported by the system)
## EXAMPLES:
+17
View File
@@ -0,0 +1,17 @@
nodelicense(8) -- Manage license keys on BMC
=================================================================
## SYNOPSIS
`nodelicense <noderange> [list|install <filename>|delete <license>|save <directory>]`
## DESCRIPTION
`nodelicense` manages license keys on supported BMCs. Without an argument, the command
lists currently installed license. Using `delete` will remove the specified license name
from th eBMC. The `save` subcommand will take the passed directory (which may be in the form
of /path/to/{node}/ to have the node name substituted for each node) and back up installed licenses
to that directory. The `install` command will take the specified filename and install. The filename
argument may be of the form xcc_fod_0034_7X21{id.serial}.key to have the serial number substituted
to allow unique licenses to be specified in a single command.
+3
View File
@@ -17,6 +17,9 @@ displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. There is more
information on node attributes in nodeattributes(5) man page.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
## OPTIONS
* `-b`, `--blame`:
+5 -2
View File
@@ -36,10 +36,13 @@ Also, regular expressions may be used to indicate nodes with names matching cert
The other major noderange primitive is indicating nodes by some attribute value:
`location.rack=7`
The attribute name may use a wildcard:
`net.*switch=switch1`
Commas can be used to indicate multiple nodes, and can mix and match any of the above primitives. The following can be a valid single noderange, combining any and all members of each comma separated component
`n1,n2,rack1,storage,location.rack=9,~s1..,n20-n30`
Exclusions can be done by prepending a ‘-‘ before a portion of a noderange:
Exclusions can be done by prepending a '-' before a portion of a noderange:
`rack1,-n2`
`compute,-rack1`
`compute,-location.row=12`
@@ -51,7 +54,7 @@ To indicate nodes that match multiple selections at once (set intersection), the
For complex expressions, () may be used to indicate order of expanding the noderange to be explicit
`rack1,-(console.logging=full@compute)`
Noderange syntax can also indicate ‘pagination’, or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
Noderange syntax can also indicate 'pagination', or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
`rack1>3<6`
The above would show the seventh through ninth nodes of the rack1 group. Like all other noderange operations, this may be combined with any of the above, but must appear as the very last operation. Ordering is done with a natural sort.
+4 -1
View File
@@ -30,7 +30,10 @@ control.
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `default`:
Request a normal default boot with no particular device override
+65
View File
@@ -0,0 +1,65 @@
nodestorage(8) -- Examine/Modify storage configuration of a node
============================================
## SYNOPSIS
`nodestorage <noderange> [show|create|delete] [options]`
## DESCRIPTION
`nodestorage` provides access to the remote storage configuration of
the noderange.
## OPTIONS
* `-r` **RAIDLEVEL**, `--raidlevel`=**RAIDLEVEL**:
RAID level to use when creating an array
* `-d` **DISKS**, `--disks`=**DISKS**:
Comma separated list of disks to use, or the word "rest" to
indicate use of all available disks
* `-s` **SIZE**, `--size`=**SIZE**:
Comma separated list of sizes to use when creating
volumes. The sizes may be absolute size (e.g. 16gb),
percentage (10%) or the word "rest" to use remaining
capacity, default behavior is to use all capacity to
make a volume
* `-n` **NAME**, `--name`=**NAME**:
Comma separated list of names to use when naming
volumes, or selecting a volume for delete. Default
behavior is to use implementation provided default
## EXAMPLES
* Deleting the volume `somedata`:
`$ nodestorage d5 delete somedata`
`Deleted: somedata`
* Creating a raid5 of 4 disks and a volume named `somedata`:
`$ nodestorage d5 create -r 5 -d drive0,drive_1,drive_2,drive_3 -n somedata`
`d5: Volume somedata: Size: 1.905 TB`
`d5: Volume somedata: State: Optimal`
`d5: Volume somedata: Array 1-2`
* Showing current storage configuration of `d3`:
`$ nodestorage d3`
`d3: Disk m.2-0 Description: 128GB M.2 SATA SSD`
`d3: Disk m.2-0 State: online`
`d3: Disk m.2-0 FRU: 00LF428`
`d3: Disk m.2-0 Serial Number: H6B80054`
`d3: Disk m.2-0 Array: 0-0`
`d3: Disk m.2-1 Description: 128GB M.2 SATA SSD`
`d3: Disk m.2-1 State: online`
`d3: Disk m.2-1 FRU: 00LF428`
`d3: Disk m.2-1 Serial Number: H6B80059`
`d3: Disk m.2-1 Array: 0-0`
`d3: Array 0-0 Available Capacity: 0.000 MB`
`d3: Array 0-0 Total Capacity: 131.072 GB`
`d3: Array 0-0 RAID: RAID 1`
`d3: Array 0-0 Disks: m.2-0,m.2-1`
`d3: Array 0-0 Volumes: new_vd`
`d3: Volume new_vd: Size: 122.040 GB`
`d3: Volume new_vd: State: Optimal`
`d3: Volume new_vd: Array 0-0`
+32
View File
@@ -0,0 +1,32 @@
nodesupport(8) -- Utilities for interacting with vendor support
=================================================================
## SYNOPSIS
`nodesupport <noderange> servicedata <directory or filename>`
## DESCRIPTION
`nodesupport` provides capabilities associated with interacting with support.
Currently it only has the `servicedata` subcommand. `servicedata` takes
an argument that is either a directory name (that can be used for a single node
or multiple nodes) or a file name (only to be used with single node noderange).
Note that the file will be downloaded to the confluent server that actually
connects to the managed system, so it will download to the remote system if running
remotely and will download to the collective.manager indicated system if
running in collective mode.
Note that due to vendor filename requirements, any filename may have vendor
specific suffixes added to any file produced.
## EXAMPLES
* Download support data from a single node to a specific filename
`# nodesupport d1 servicedata svcdata.out`
`d1:initializing: 15%`
* Download support data from multiple nodes to a directory
`# nodesupport d1-d4 servicedata service/`
`d1:initializing: 0% d2:initializing: 0% d3:initializing: 0% d4:initializing: 0%`
`# ls service/`
`d1.svcdata d2.svcdata d3.svcdata d4.svcdata`
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# This is a sample python script for going through all observed mac addresses
# and assuming they are BMC related and printing nodeattrib commands
+2 -2
View File
@@ -22,10 +22,10 @@ This provides the modules common for both client and server
%setup -n %{name}-%{version} -n %{name}-%{version}
%build
python setup.py build
python2 setup.py build
%install
python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
python2 setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-purelib=/opt/confluent/lib/python --install-scripts=/opt/confluent/bin
%clean
rm -rf $RPM_BUILD_ROOT
-17
View File
@@ -1,17 +0,0 @@
[metadata]
name = confluent_common
summary = Confluent Common Libraries
description-file =
README.txt
author = Jarrod Johnson
author-email = jjohnson2@lenovo.com
home-page = http://xcat.sf.net/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
[files]
+1
View File
@@ -1,3 +1,4 @@
include pam/*
include sysvinit/*
include systemd/*
include sysctl/*
+43 -9
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
import argparse
import errno
@@ -22,7 +22,7 @@ except NameError:
pass
def make_certificate():
umask = os.umask(0077)
umask = os.umask(0o77)
try:
os.makedirs('/etc/confluent/cfg')
except OSError as e:
@@ -61,20 +61,50 @@ def join_collective(server, invitation):
make_certificate()
s = client.Command().connection
while not invitation:
invitation = raw_input('Paste the invitation here: ')
invitation = input('Paste the invitation here: ')
tlvdata.send(s, {'collective': {'operation': 'join',
'invitation': invitation,
'server': server}})
res = tlvdata.recv(s)
print(res.get('collective',
{'status': 'Unknown response: ' + repr(res)})['status'])
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',
'member': name}})
res = 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 show_collective():
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'show'}})
res = tlvdata.recv(s)
print(repr(res))
if 'error' in res['collective']:
print(res['collective']['error'])
return
if 'quorum' in res['collective']:
print('Quorum: {0}'.format(res['collective']['quorum']))
print('Leader: {0}'.format(res['collective']['leader']))
if 'active' in res['collective']:
if res['collective']['active']:
print('Active collective members:')
for member in res['collective']['active']:
print(' {0}'.format(member))
if res['collective']['offline']:
print('Offline collective members:')
for member in res['collective']['offline']:
print(' {0}'.format(member))
else:
print('Run collective show on leader for more data')
def main():
a = argparse.ArgumentParser(description='Confluent server utility')
@@ -84,11 +114,13 @@ def main():
sl = sp.add_parser('show', help='Show information about the collective')
ic = sp.add_parser('invite', help='Generate a invitation to allow a new '
'confluent instance to join as a '
'collective member')
'collective member. Run collective invite -h for more information')
ic.add_argument('name', help='Name of server to invite to join the '
'collective')
jc = sp.add_parser('join', help='Join a collective')
jc.add_argument('server', help='A server currently in the collective')
dc = sp.add_parser('delete', help='Delete a member of a collective')
dc.add_argument('name', help='Name of server to delete from collective')
jc = sp.add_parser('join', help='Join a collective. Run collective join -h for more information')
jc.add_argument('server', help='Existing collective member that ran invite and generated a token')
jc.add_argument('-i', help='Invitation provided by runniing invite on an '
'existing collective member')
cmdset = a.parse_args()
@@ -100,6 +132,8 @@ def main():
join_collective(cmdset.server, cmdset.i)
elif cmdset.command == 'show':
show_collective()
elif cmdset.command == 'delete':
delete_member(cmdset.name)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+55
View File
@@ -0,0 +1,55 @@
from os.path import exists
import shutil
import socket
import subprocess
import tempfile
def get_openssl_conf_location():
if exists('/etc/pki/tls/openssl.cnf'):
return '/etc/pki/tls/openssl.cnf'
elif exists('/etc/ssl/openssl.cnf');
return '/etc/ssl/openssl.cnf'
else:
raise Exception("Cannot find openssl config file")
def get_ip_addresses():
lines = subprocess.check_output('ip addr'.split(' '))
for line in lines.split('\n'):
if line.startswith(' inet6 '):
line = line.replace(' inet6 ', '').split('/')[0]
if line.startswith('fe80::'):
continue
if line == '::1':
continue
elif line.startswith(' inet '):
line = line.replace(' inet ', '').split('/')[0]
if line == '127.0.0.1':
continue
if line.startswith('169.254.'):
continue
else:
continue
yield line
def create_certificate():
shortname = socket.gethostname().split('.')[0]
longname = socket.getfqdn()
subprocess.check_call(
'openssl ecparam -name secp384r1 -genkey -out privkey.pem'.split(' '))
san = ['IP:{0}'.format(x) for x in get_ip_addresses()]
san.append('DNS:{0}'.format(shortname))
san.append('DNS:{0}'.format(longname))
san = ','.join(san)
sslcfg = get_openssl_conf_location()
tmpconfig = tempfile.mktemp()
shutil.copy2(sslcfg, tmpconfig)
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
subprocess.check_call(
'openssl req -new -x509 -key privkey.pem -days 7300 -out cert.pem '
'-subj /CN={0} -extensions SAN '
'-config {1}'.format(longname, tmpconfig).split(' ')
)
if __name__ == '__main__':
create_certificate()
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -82,7 +82,7 @@ elif args[0] == 'dump':
"or -s to do encrypted backup that requires keys.json from "
"another backup to restore.")
sys.exit(1)
os.umask(077)
os.umask(0o77)
main._initsecurity(conf.get_config())
if not os.path.exists(dumpdir):
os.makedirs(dumpdir)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
+68
View File
@@ -0,0 +1,68 @@
#!/bin/bash
cd `dirname $0`
PKGNAME=$(basename $(pwd))
DPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
OPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
PYEXEC=python3
DSCARGS="--with-python3=True --with-python2=False"
if grep wheezy /etc/os-release; then
DPKGNAME=python-$DPKGNAME
PYEXEC=python
DSCARGS="--with-python3=False --with-python2=True"
fi
cd ..
mkdir -p /tmp/confluent # $DPKGNAME
cp -a * .git /tmp/confluent # $DPKGNAME
cd /tmp/confluent/$PKGNAME
if [ -x ./makeman ]; then
./makeman
fi
./makesetup
VERSION=`cat VERSION`
cat > setup.cfg << EOF
[install]
install-purelib=/opt/confluent/lib/python
install-scripts=/opt/confluent/bin
[sdist_dsc]
package=$DPKGNAME
EOF
$PYEXEC setup.py sdist > /dev/null 2>&1
py2dsc $DSCARGS dist/*.tar.gz
shopt -s extglob
cd deb_dist/!(*.orig)/
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/' debian/control
else
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket/' debian/control
fi
if grep wheezy /etc/os-release; then
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
else
echo 'confluent_client confluent-client' >> debian/pydist-overrides
fi
fi
head -n -1 debian/control > debian/control1
mv debian/control1 debian/control
echo 'export PYBUILD_INSTALL_ARGS=--install-lib=/opt/confluent/lib/python' >> debian/rules
#echo 'Provides: python-'$DPKGNAME >> debian/control
#echo 'Conflicts: python-'$DPKGNAME >> debian/control
#echo 'Replaces: python-'$DPKGNAME' (<<2)' >> debian/control
#echo 'Breaks: python-'$DPKGNAME' (<<2)' >> debian/control
dpkg-buildpackage -rfakeroot -uc -us -i
if [ $? -ne 0 ]; then
echo "[ERROR] rpmbuild returned non-zero, run: rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec"
exit 1
else
cd -
# Clean up the generated files in this directory
rm -rf $PKGNAME.egg-info dist setup.py
rm -rf $(find deb_dist -mindepth 1 -maxdepth 1 -type d)
if [ ! -z "$1" ]; then
mv deb_dist/* $1/
fi
fi
exit 0
+1 -1
View File
@@ -6,7 +6,7 @@ fi
./makesetup
VERSION=`cat VERSION`
PKGNAME=$(basename $(pwd))
python setup.py sdist > /dev/null 2>&1
python3 setup.py sdist > /dev/null 2>&1
cp dist/*.tar.gz ~/rpmbuild/SOURCES
sed -e 's/#VERSION#/'$VERSION/ $PKGNAME.spec.tmpl > ~/rpmbuild/SPECS/$PKGNAME.spec
rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec 2> /dev/null |grep ^Wrote:
+7 -1
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
# Copyright 2016-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -83,6 +83,8 @@ class AsyncSession(object):
self.reaper = eventlet.spawn_after(15, self.destroy)
def add(self, requestid, rsp):
if self.responses is None:
return
self.responses.append((requestid, rsp))
if self._evt:
self._evt.send()
@@ -107,6 +109,7 @@ class AsyncSession(object):
for console in self.consoles:
_consolesessions[console]['session'].destroy()
self.consoles = None
self.responses = None
del _asyncsessions[self.asyncid]
def run_handler(self, handler, requestid):
@@ -166,6 +169,9 @@ def handle_async(env, querydict, threadset):
currsess = AsyncSession()
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
+140 -77
View File
@@ -23,11 +23,15 @@ import confluent.config.configmanager as configmanager
import eventlet
import eventlet.tpool
import Cryptodome.Protocol.KDF as KDF
from fnmatch import fnmatch
import hashlib
import hmac
import multiprocessing
import confluent.userutil as userutil
import confluent.util as util
pam = None
try:
import PAM
import confluent.pam as pam
except ImportError:
pass
import time
@@ -39,24 +43,59 @@ _passchecking = {}
authworkers = None
authcleaner = None
_allowedbyrole = {
'Operator': {
'retrieve': ['*'],
'create': [
'/noderange/',
'/nodes/',
'/node*/media/uploads/',
'/node*/inventory/firmware/updates/*',
'/node*/suppport/servicedata*',
'/node*/attributes/expression',
'/nodes/*/console/session*',
'/nodes/*/shell/sessions*',
'/node*/configuration/*',
],
'update': [
'/discovery/*',
'/networking/macs/rescan',
'/node*/power/state',
'/node*/power/reseat',
'/node*/attributes/*',
'/node*/media/*tach',
'/node*/boot/nextdevice',
'/node*/identify',
'/node*/configuration/*',
],
'start': [
'/nodes/*/console/session*',
'/nodes/*/shell/sessions*',
],
'delete': [
'/discovery/*',
'/node*',
],
},
'Monitor': {
'retrieve': [
'/node*/health/hardware',
'/node*/power/state',
'/node*/sensors/*',
'/nodes/',
'/',
],
}
}
class Credentials(object):
def __init__(self, username, passphrase):
self.username = username
self.passphrase = passphrase
self.haspam = False
def pam_conv(self, auth, query_list):
# use stored credentials in a pam conversation
self.haspam = True
resp = []
for query_entry in query_list:
query, pamtype = query_entry
if query.startswith('Password'):
resp.append((self.passphrase, 0))
else:
return None
return resp
_deniedbyrole = {
# This supersedes the above and is only consulted after the allowed has happened
'Operator': {
'update': [
'/node*/configuration/management_controller/users/*',
]
}
}
def _prune_passcache():
@@ -65,9 +104,9 @@ def _prune_passcache():
while True:
curtime = time.time()
for passent in _passcache.iterkeys():
if passent[2] < curtime - 10:
if passent[2] < curtime - 90:
del _passcache[passent]
eventlet.sleep(10)
eventlet.sleep(90)
def _get_usertenant(name, tenant=False):
@@ -81,11 +120,13 @@ def _get_usertenant(name, tenant=False):
administrator account a tenant gets.
Otherwise, just assume a user in the default tenant
"""
if not isinstance(name, bytes):
name = name.encode('utf-8')
if not isinstance(tenant, bool):
# if not boolean, it must be explicit tenant
user = name
elif '/' in name: # tenant scoped name
tenant, user = name.split('/', 1)
elif b'/' in name: # tenant scoped name
tenant, user = name.split(b'/', 1)
elif configmanager.is_tenant(name):
# the account is the implicit tenant owner account
user = name
@@ -93,6 +134,9 @@ def _get_usertenant(name, tenant=False):
else: # assume it is a non-tenant user account
user = name
tenant = None
user = util.stringify(user)
if tenant:
tenant = util.stringify(tenant)
yield user
yield tenant
@@ -112,21 +156,37 @@ def authorize(name, element, tenant=False, operation='create',
and the relevant ConfigManager object for the context of the
request.
"""
if operation not in ('create', 'start', 'update', 'retrieve', 'delete'):
return None
# skipuserobj is a leftover from the now abandoned plan to use pam session
# to do authorization and authentication. Now confluent always does authorization
# even if pam does authentication.
if operation not in ('create', 'start', 'update', 'retrieve', 'delete', None):
return False
user, tenant = _get_usertenant(name, tenant)
if tenant is not None and not configmanager.is_tenant(tenant):
return None
return False
manager = configmanager.ConfigManager(tenant, username=user)
if skipuserobj:
return None, manager, user, tenant, skipuserobj
userobj = manager.get_user(user)
if not userobj:
for group in userutil.grouplist(user):
userobj = manager.get_usergroup(group)
if userobj:
break
if userobj: # returning
role = userobj.get('role', 'Administrator')
if element and role != 'Administrator':
for rule in _allowedbyrole.get(role, {}).get(operation, []):
if fnmatch(element, rule):
break
else:
return False
for rule in _deniedbyrole.get(role, {}).get(operation, []):
if fnmatch(element, rule):
return False
return userobj, manager, user, tenant, skipuserobj
return None
return False
def check_user_passphrase(name, passphrase, element=None, tenant=False):
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.
@@ -157,63 +217,66 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# would normally make an event and wait
# but here there's no need for that
eventlet.sleep(0.5)
credobj = Credentials(user, passphrase)
try:
pammy = PAM.pam()
pammy.start(_pamservice, user, credobj.pam_conv)
pammy.authenticate()
pammy.acct_mgmt()
del pammy
return authorize(user, element, tenant, skipuserobj=False)
except NameError:
pass
except PAM.error:
if credobj.haspam:
return None
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None:
try:
for group in userutil.grouplist(user):
ucfg = cfm.get_usergroup(group)
if ucfg:
break
except KeyError:
pass
if ucfg is None:
eventlet.sleep(0.05)
return None
if (user, tenant) in _passcache:
if passphrase == _passcache[(user, tenant)]:
return authorize(user, element, tenant)
if hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
return authorize(user, element, tenant, operation=operation)
else:
# In case of someone trying to guess,
# while someone is legitimately logged in
# invalidate cache and force the slower check
del _passcache[(user, tenant)]
return None
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None or 'cryptpass' not in ucfg:
eventlet.sleep(0.05) # stall even on test for existence of a username
return None
_passchecking[(user, tenant)] = True
# TODO(jbjohnso): WORKERPOOL
# PBKDF2 is, by design, cpu intensive
# 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)
else:
authcleaner.cancel()
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
del _passchecking[(user, tenant)]
eventlet.sleep(0.05) # either way, we want to stall so that client can't
if 'cryptpass' in ucfg:
_passchecking[(user, tenant)] = True
# TODO(jbjohnso): WORKERPOOL
# PBKDF2 is, by design, cpu intensive
# 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)
else:
authcleaner.cancel()
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
del _passchecking[(user, tenant)]
eventlet.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
if crypt == crypted:
_passcache[(user, tenant)] = passphrase
return authorize(user, element, tenant)
if crypt == crypted:
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant, operation)
if pam:
pammy = pam.pam()
usergood = pammy.authenticate(user, passphrase)
del pammy
if usergood:
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant, operation, skipuserobj=False)
eventlet.sleep(0.05) # stall even on test for existence of a username
return None
def _apply_pbkdf(passphrase, salt):
return KDF.PBKDF2(passphrase, salt, 32, 10000,
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
@@ -231,4 +294,4 @@ def _do_pbkdf(passphrase, salt):
# 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])
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
+256 -66
View File
@@ -18,13 +18,16 @@ import base64
import confluent.collective.invites as invites
import confluent.config.configmanager as cfm
import confluent.exceptions as exc
import confluent.log as log
import confluent.tlvdata as tlvdata
import confluent.util as util
import eventlet
import eventlet.green.socket as socket
import eventlet.green.ssl as ssl
import eventlet.green.threading as threading
import greenlet
import random
import sys
try:
import OpenSSL.crypto as crypto
except ImportError:
@@ -33,41 +36,57 @@ except ImportError:
crypto = None
currentleader = None
cfginitlock = None
follower = None
retrythread = None
class ContextBool(object):
def __init__(self):
self.active = False
self.mylock = threading.RLock()
def __enter__(self):
self.active = True
self.mylock.__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
self.active = False
self.mylock.__exit__(exc_type, exc_val, exc_tb)
connecting = ContextBool()
leader_init = ContextBool()
def connect_to_leader(cert=None, name=None, leader=None):
global currentleader
global cfginitlock
global follower
if cfginitlock is None:
cfginitlock = threading.RLock()
if leader is None:
leader = currentleader
log.log({'info': 'Attempting connection to leader {0}'.format(leader),
'subsystem': 'collective'})
try:
remote = connect_to_collective(cert, leader)
except socket.error:
except socket.error as e:
log.log({'error': 'Collective connection attempt to {0} failed: {1}'
''.format(leader, str(e)),
'subsystem': 'collective'})
return False
with connecting:
with cfginitlock:
tlvdata.recv(remote) # the banner
with cfm._initlock:
banner = tlvdata.recv(remote) # the banner
vers = banner.split()[2]
pvers = 0
reqver = 4
if vers == b'v0':
pvers = 2
elif vers == b'v1':
pvers = 4
if sys.version_info[0] < 3:
pvers = 2
reqver = 2
tlvdata.recv(remote) # authpassed... 0..
if name is None:
name = get_myname()
tlvdata.send(remote, {'collective': {'operation': 'connect',
'protover': reqver,
'name': name,
'txcount': cfm._txcount}})
keydata = tlvdata.recv(remote)
@@ -75,10 +94,16 @@ def connect_to_leader(cert=None, name=None, leader=None):
return False
if 'error' in keydata:
if 'backoff' in keydata:
eventlet.spawn_after(random.random(), connect_to_leader,
cert, name, leader)
return True
log.log({
'info': 'Collective initialization in progress on '
'{0}'.format(leader),
'subsystem': 'collective'})
return False
if 'leader' in keydata:
log.log(
{'info': 'Prospective leader {0} has redirected this '
'member to {1}'.format(leader, keydata['leader']),
'subsystem': 'collective'})
ldrc = cfm.get_collective_member_by_address(
keydata['leader'])
if ldrc and ldrc['name'] == name:
@@ -86,18 +111,27 @@ def connect_to_leader(cert=None, name=None, leader=None):
return connect_to_leader(name=name,
leader=keydata['leader'])
if 'txcount' in keydata:
log.log({'info':
'Prospective leader {0} has inferior '
'transaction count, becoming leader'
''.format(leader), 'subsystem': 'collective',
'subsystem': 'collective'})
return become_leader(remote)
print(keydata['error'])
return False
if follower is not None:
follower.kill()
cfm.stop_following()
follower = None
if follower:
follower.kill()
cfm.stop_following()
follower = None
log.log({'info': 'Following leader {0}'.format(leader),
'subsystem': 'collective'})
colldata = tlvdata.recv(remote)
globaldata = tlvdata.recv(remote)
dbi = tlvdata.recv(remote)
dbsize = dbi['dbsize']
dbjson = ''
dbjson = b''
while (len(dbjson) < dbsize):
ndata = remote.recv(dbsize - len(dbjson))
if not ndata:
@@ -126,17 +160,28 @@ def connect_to_leader(cert=None, name=None, leader=None):
raise
currentleader = leader
#spawn this as a thread...
follower = eventlet.spawn(follow_leader, remote)
follower = eventlet.spawn(follow_leader, remote, pvers, leader)
return True
def follow_leader(remote):
def follow_leader(remote, proto, leader):
global currentleader
cfm.follow_channel(remote)
# The leader has folded, time to startup again...
cfm.stop_following()
currentleader = None
eventlet.spawn_n(start_collective)
cleanexit = False
try:
cfm.follow_channel(remote, proto)
except greenlet.GreenletExit:
cleanexit = True
finally:
if cleanexit:
log.log({'info': 'Previous following cleanly closed',
'subsystem': 'collective'})
return
log.log({'info': 'Current leader ({0}) has disappeared, restarting '
'collective membership'.format(leader), 'subsystem': 'collective'})
# The leader has folded, time to startup again...
cfm.stop_following()
currentleader = None
eventlet.spawn_n(start_collective)
def connect_to_collective(cert, member):
@@ -168,26 +213,67 @@ def get_myname():
def handle_connection(connection, cert, request, local=False):
global currentleader
global retrythread
operation = request['operation']
if cert:
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
else:
if not local:
return
if 'show' == operation:
if operation in ('show', 'delete'):
if not list(cfm.list_collective()):
tlvdata.send(connection,
{'collective': {'error': 'Collective mode not '
'enabled on this '
'system'}})
return
if follower:
linfo = cfm.get_collective_member_by_address(currentleader)
remote = socket.create_connection((currentleader, 13001))
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
cert = remote.getpeercert(binary_form=True)
if not (linfo and util.cert_matches(
linfo['fingerprint'],
cert)):
remote.close()
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
tlvdata.recv(remote) # ignore banner
tlvdata.recv(remote) # ignore authpassed: 0
tlvdata.send(remote,
{'collective': {'operation': 'getinfo',
'name': get_myname()}})
collinfo = tlvdata.recv(remote)
else:
collinfo = {}
populate_collinfo(collinfo)
try:
cfm.check_quorum()
collinfo['quorum'] = True
except exc.DegradedCollective:
collinfo['quorum'] = False
if operation == 'show':
tlvdata.send(connection, {'collective': collinfo})
elif operation == 'delete':
todelete = request['member']
if (todelete == collinfo['leader'] or
todelete in collinfo['active']):
tlvdata.send(connection, {'collective':
{'error': '{0} is still active, stop the confluent service to remove it'.format(todelete)}})
return
if todelete not in collinfo['offline']:
tlvdata.send(connection, {'collective':
{'error': '{0} is not a recognized collective member'.format(todelete)}})
return
cfm.del_collective_member(todelete)
tlvdata.send(connection,
{'collective':
{'error': 'Collective does not have quorum'}})
if follower:
myleader = cfm.get_collective_member_by_address(
currentleader)['name']
else:
myleader = get_myname()
tlvdata.send(connection, {'collective': {'leader': myleader}})
{'collective': {'status': 'Successfully deleted {0}'.format(todelete)}})
connection.close()
return
if 'invite' == operation:
try:
@@ -208,7 +294,8 @@ def handle_connection(connection, cert, request, local=False):
invitation = request['invitation']
try:
invitation = base64.b64decode(invitation)
name, invitation = invitation.split('@', 1)
name, invitation = invitation.split(b'@', 1)
name = util.stringify(name)
except Exception:
tlvdata.send(
connection,
@@ -263,6 +350,8 @@ def handle_connection(connection, cert, request, local=False):
f = open('/etc/confluent/cfg/myname', 'w')
f.write(name)
f.close()
log.log({'info': 'Connecting to collective due to join',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, rsp['collective'][
'fingerprint'], name)
if 'enroll' == operation:
@@ -292,6 +381,11 @@ def handle_connection(connection, cert, request, local=False):
if 'assimilate' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not droneinfo:
tlvdata.send(connection,
{'error': 'Unrecognized leader, '
'redo invitation process'})
return
if not util.cert_matches(droneinfo['fingerprint'], cert):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
@@ -301,26 +395,27 @@ def handle_connection(connection, cert, request, local=False):
tlvdata.send(connection,
{'error': 'Refusing to be assimilated by inferior'
'transaction count',
'txcount': cfm._txcount})
'txcount': cfm._txcount,})
return
if connecting.active:
# don't try to connect while actively already trying to connect
tlvdata.send(connection, {'status': 0})
connection.close()
return
if (currentleader == connection.getpeername()[0] and
follower and not follower.dead):
# if we are happily following this leader already, don't stir
# the pot
tlvdata.send(connection, {'status': 0})
connection.close()
return
log.log({'info': 'Connecting in response to assimilation',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, None, None,
leader=connection.getpeername()[0])
tlvdata.send(connection, {'status': 0})
connection.close()
if 'connect' == operation:
myself = connection.getsockname()[0]
if myself != get_leader(connection):
tlvdata.send(
connection,
{'error': 'Cannot assimilate, our leader is '
'in another castle', 'leader': currentleader})
connection.close()
return
if connecting.active:
tlvdata.send(connection, {'error': 'Connecting right now',
'backoff': True})
connection.close()
return
if 'getinfo' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
@@ -330,29 +425,81 @@ def handle_connection(connection, cert, request, local=False):
'redo invitation process'})
connection.close()
return
collinfo = {}
populate_collinfo(collinfo)
tlvdata.send(connection, collinfo)
if 'connect' == operation:
drone = request['name']
folver = request.get('protover', 2)
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
cert)):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
myself = connection.getsockname()[0]
if connecting.active:
tlvdata.send(connection, {'error': 'Connecting right now',
'backoff': True})
connection.close()
return
if myself != get_leader(connection):
tlvdata.send(
connection,
{'error': 'Cannot assimilate, our leader is '
'in another castle', 'leader': currentleader})
connection.close()
return
if request['txcount'] > cfm._txcount:
retire_as_leader()
tlvdata.send(connection,
{'error': 'Client has higher tranasaction count, '
'should assimilate me, connecting..',
'txcount': cfm._txcount})
log.log({'info': 'Connecting to leader due to superior '
'transaction count', 'subsystem': collective})
eventlet.spawn_n(connect_to_leader, None, None,
connection.getpeername()[0])
connection.close()
return
tlvdata.send(connection, cfm._dump_keys(None, False))
tlvdata.send(connection, cfm._cfgstore['collective'])
tlvdata.send(connection, cfm.get_globals())
cfgdata = cfm.ConfigManager(None)._dump_to_json()
tlvdata.send(connection, {'txcount': cfm._txcount,
'dbsize': len(cfgdata)})
connection.sendall(cfgdata)
if retrythread:
retrythread.cancel()
retrythread = None
with leader_init:
cfm.update_collective_address(request['name'],
connection.getpeername()[0])
tlvdata.send(connection, cfm._dump_keys(None, False))
tlvdata.send(connection, cfm._cfgstore['collective'])
tlvdata.send(connection, cfm.get_globals())
cfgdata = cfm.ConfigManager(None)._dump_to_json()
tlvdata.send(connection, {'txcount': cfm._txcount,
'dbsize': len(cfgdata)})
connection.sendall(cfgdata)
#tlvdata.send(connection, {'tenants': 0}) # skip the tenants for now,
# so far unused anyway
cfm.relay_slaved_requests(drone, connection)
if not cfm.relay_slaved_requests(drone, connection, folver):
if not retrythread: # start a recovery if everyone else seems
# to have disappeared
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
# ok, we have a connecting member whose certificate checks out
# He needs to bootstrap his configuration and subscribe it to updates
def populate_collinfo(collinfo):
iam = get_myname()
collinfo['leader'] = iam
collinfo['active'] = list(cfm.cfgstreams)
activemembers = set(cfm.cfgstreams)
activemembers.add(iam)
collinfo['offline'] = []
for member in cfm.list_collective():
if member not in activemembers:
collinfo['offline'].append(member)
def try_assimilate(drone):
try:
remote = connect_to_collective(None, drone)
@@ -366,11 +513,36 @@ def try_assimilate(drone):
tlvdata.recv(remote) # the banner
tlvdata.recv(remote) # authpassed... 0..
answer = tlvdata.recv(remote)
if answer and 'error' in answer:
if not answer:
log.log(
{'error':
'No answer from {0} while trying to assimilate'.format(
drone),
'subsystem': 'collective'})
return
if 'txcount' in answer:
log.log({'info': 'Deferring to {0} due to transaction count'.format(
drone), 'subsystem': 'collective'})
connect_to_leader(None, None, leader=remote.getpeername()[0])
return
if 'error' in answer:
log.log({
'error': 'Error encountered while attempting to '
'assimilate {0}: {1}'.format(drone, answer['error']),
'subsystem': 'collective'})
return
log.log({'info': 'Assimilated {0} into collective'.format(drone),
'subsystem': 'collective'})
def get_leader(connection):
if currentleader is None or connection.getpeername()[0] == currentleader:
if currentleader is None:
msg = 'Becoming leader as no leader known'
else:
msg = 'Becoming leader because {0} attempted to connect and it ' \
'is current leader'.format(currentleader)
log.log({'info': msg, 'subsystem': 'collective'})
become_leader(connection)
return currentleader
@@ -382,48 +554,66 @@ def retire_as_leader():
def become_leader(connection):
global currentleader
global follower
global retrythread
log.log({'info': 'Becoming leader of collective',
'subsystem': 'collective'})
if follower:
follower.kill()
cfm.stop_following()
follower = None
if retrythread:
retrythread.cancel()
retrythread = None
currentleader = connection.getsockname()[0]
skipaddr = connection.getpeername()[0]
myname = get_myname()
skipem = set(cfm.cfgstreams)
skipem.add(currentleader)
skipem.add(skipaddr)
for member in cfm.list_collective():
dronecandidate = cfm.get_collective_member(member)['address']
if dronecandidate in (currentleader, skipaddr) or member == myname:
if dronecandidate in skipem or member == myname:
continue
eventlet.spawn_n(try_assimilate, dronecandidate)
def startup():
global cfginitlock
members = list(cfm.list_collective())
if len(members) < 2:
# Not in collective mode, return
return
if cfginitlock is None:
cfginitlock = threading.RLock()
eventlet.spawn_n(start_collective)
def start_collective():
global follower
global retrythread
if follower:
follower.kill()
cfm.stop_following()
follower = None
try:
if cfm.cfgstreams:
cfm.check_quorum()
# Do not start if we have quorum and are leader
return
except exc.DegradedCollective:
pass
if leader_init.active: # do not start trying to connect if we are
# xmitting data to a follower
return
myname = get_myname()
for member in cfm.list_collective():
for member in sorted(list(cfm.list_collective())):
if member == myname:
continue
if cfm.cfgleader is None:
cfm.stop_following(True)
ldrcandidate = cfm.get_collective_member(member)['address']
log.log({'info': 'Performing startup attempt to {0}'.format(
ldrcandidate), 'subsystem': 'collective'})
if connect_to_leader(name=myname, leader=ldrcandidate):
break
else:
for member in cfm.list_collective():
if member == myname:
continue
eventlet.spawn_n(try_assimilate,
cfm.get_collective_member(member)['address'])
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# 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.
@@ -93,9 +93,10 @@ node = {
'description': ('List of static groups for which this node is '
'considered a member'),
},
#'type': {
# 'description': ('Classification of node as system, vm, etc')
#},
'type': {
'description': ('Classification of node as server or switch'),
'validvalues': ('switch', 'server'),
},
#'id': {
# 'description': ('Numeric identifier for node')
#},
@@ -156,6 +157,14 @@ node = {
'indicates candidate managers, either for '
'high availability or load balancing purposes.')
},
'discovery.passwordrules': {
'description': 'Any specified rules shall be configured on the BMC '
'upon discovery. "expiration=no,loginfailures=no,complexity=no,reuse=no" '
'would disable password expiration, login failures '
'triggering a lockout, password complexity requirements,'
'and any restrictions around reusing an old password.',
'validlistkeys': ('expiration', 'loginfailures', 'complexity', 'reuse'),
},
'discovery.policy': {
'description': 'Policy to use for auto-configuration of discovered '
'and identified nodes. Valid values are "manual", '
@@ -165,6 +174,7 @@ node = {
'so long as the node has no existing public key. '
'"open" allows discovery even if a known public key '
'is already stored',
'validlist': ('manual', 'permissive', 'pxe', 'open'),
},
'info.note': {
'description': 'A field used for administrators to make arbitrary '
@@ -173,6 +183,9 @@ node = {
'freeform text data without concern for issues in how '
'the server will process it.',
},
'location.height': {
'description': 'Height in RU of the system (defaults to query the systems)',
},
'location.room': {
'description': 'Room description for the node',
},
@@ -252,11 +265,13 @@ node = {
'console.logging': {
'description': ('Indicate logging level to apply to console. Valid '
'values are currently "full", "interactive", and '
'"none". Defaults to "full".')
'"none". Defaults to "full".'),
'validvalues': ('full', 'interactive', 'none'),
},
'console.method': {
'description': ('Indicate the method used to access the console of '
'the managed node.')
'the managed node.'),
'validvalues': ('ssh', 'ipmi', 'tsmsol'),
},
# 'virtualization.host': {
# 'description': ('Hypervisor where this node does/should reside'),
+4 -1
View File
@@ -16,7 +16,10 @@
# This defines config variable to store the global configuration for confluent
import ConfigParser
try:
import ConfigParser
except ModuleNotFoundError:
import configparser as ConfigParser
import os
_config = None
File diff suppressed because it is too large Load Diff
+102 -42
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2017 Lenovo
# Copyright 2017-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -94,7 +94,7 @@ def _utf8_normalize(data, shiftin, decoder):
def pytechars2line(chars, maxlen=None):
line = '\x1b[m' # start at default params
line = b'\x1b[m' # start at default params
lb = False # last bold
li = False # last italic
lu = False # last underline
@@ -106,7 +106,7 @@ def pytechars2line(chars, maxlen=None):
len = 1
for charidx in range(maxlen):
char = chars[charidx]
csi = []
csi = bytearray([])
if char.fg != lfg:
csi.append(30 + pytecolors2ansi[char.fg])
lfg = char.fg
@@ -129,10 +129,13 @@ def pytechars2line(chars, maxlen=None):
lr = char.reverse
csi.append(7 if lr else 27)
if csi:
line += b'\x1b[' + b';'.join(['{0}'.format(x) for x in csi]) + b'm'
if not hasdata and char.data.encode('utf-8').rstrip():
line += b'\x1b[' + b';'.join(['{0}'.format(x).encode('utf-8') for x in csi]) + b'm'
if not hasdata and char.data.rstrip():
hasdata = True
line += char.data.encode('utf-8')
chardata = char.data
if not isinstance(chardata, bytes):
chardata = chardata.encode('utf-8')
line += chardata
if maxlen and len >= maxlen:
break
len += 1
@@ -145,8 +148,10 @@ class ConsoleHandler(object):
_genwatchattribs = frozenset(('console.method', 'console.logging',
'collective.manager'))
def __init__(self, node, configmanager):
def __init__(self, node, configmanager, width=80, height=24):
self.clearpending = False
self.clearerror = False
self.initsize = (width, height)
self._dologging = True
self._is_local = True
self._isondemand = False
@@ -183,7 +188,7 @@ class ConsoleHandler(object):
if termstate & 1:
self.appmodedetected = True
if termstate & 2:
self.shiftin = '0'
self.shiftin = b'0'
self.users = {}
self._attribwatcher = None
self._console = None
@@ -208,6 +213,8 @@ class ConsoleHandler(object):
return retrytime + (retrytime * random.random())
def feedbuffer(self, data):
if not isinstance(data, bytes):
data = data.encode('utf-8')
try:
self.termstream.feed(data)
except StopIteration: # corrupt parser state, start over
@@ -307,8 +314,11 @@ class ConsoleHandler(object):
def clearbuffer(self):
self.feedbuffer(
'\x1bc[no replay buffer due to console.logging attribute set to '
'none or interactive,\r\nconnection loss, or service restart]')
'\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 ' \
'being enabled or incorrectly configured in the OS or\r\nfirmware, ' \
'or the console simply not having any output since last connection]')
self.clearpending = True
def _detach(self):
@@ -352,23 +362,40 @@ class ConsoleHandler(object):
if self.reconnect:
self.reconnect.cancel()
self.reconnect = None
strerror = ('The console.method attribute for this node is '
'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]
except (exc.NotImplementedException, exc.NotFoundException):
self._console = None
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
except Exception as e:
strerror = str(e)
if _tracelog:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
else:
print(traceback.format_exc())
if not isinstance(self._console, conapi.Console):
self.clearbuffer()
self.connectstate = 'unconnected'
self.error = 'misconfigured'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
self.feedbuffer(
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
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')
self.send_break = self._console.send_break
self.resize = self._console.resize
if self._attribwatcher:
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
@@ -380,6 +407,7 @@ class ConsoleHandler(object):
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
try:
self.resize(width=self.initsize[0], height=self.initsize[1])
self._console.connect(self.get_console_output)
except exc.TargetEndpointBadCredentials:
self.clearbuffer()
@@ -425,6 +453,7 @@ class ConsoleHandler(object):
def _got_disconnected(self):
if self.connectstate != 'unconnected':
self._console.close()
self.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
@@ -502,33 +531,37 @@ class ConsoleHandler(object):
if data == conapi.ConsoleEvent.Disconnect:
self._got_disconnected()
return
elif data == '':
elif data in (b'', u''):
# ignore empty strings from a cconsole provider
return
if '\x1b[?1l' in data: # request for ansi mode cursor keys
if not isinstance(data, bytes):
data = data.encode('utf-8')
if b'\x1b[?1l' in data: # request for ansi mode cursor keys
self.appmodedetected = False
if '\x1b[?1h' in data: # remember the session wants the client to use
if b'\x1b[?1h' in data: # remember the session wants the client to use
# 'application mode' Thus far only observed on esxi
self.appmodedetected = True
if '\x1b)0' in data:
if b'\x1b)0' in data:
# console indicates it wants access to special drawing characters
self.shiftin = '0'
self.shiftin = b'0'
eventdata = 0
if self.appmodedetected:
eventdata |= 1
if self.shiftin is not None:
eventdata |= 2
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
# TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
if self.clearpending:
if self.clearpending or self.clearerror:
self.clearpending = False
self.feedbuffer(b'\x1bc')
self._send_rcpts(b'\x1bc')
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.shiftin, self.utf8decoder))
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
def _send_rcpts(self, data):
for rcpt in list(self.livesessions):
@@ -563,25 +596,30 @@ class ConsoleHandler(object):
if pendingbl:
retdata += pendingbl
pendingbl = b''
retdata += nline + '\r\n'
retdata += nline + b'\r\n'
else:
pendingbl += nline + '\r\n'
pendingbl += nline + b'\r\n'
if len(retdata) > 6:
retdata = retdata[:-2] # remove the last \r\n
retdata += b'\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
self.buffer.cursor.x + 1)
cursordata = '\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
self.buffer.cursor.x + 1)
if not isinstance(cursordata, bytes):
cursordata = cursordata.encode('utf-8')
retdata += cursordata
if self.shiftin is not None: # detected that terminal requested a
# shiftin character set, relay that to the terminal that cannected
retdata += '\x1b)' + self.shiftin
retdata += b'\x1b)' + self.shiftin
if self.appmodedetected:
retdata += '\x1b[?1h'
retdata += b'\x1b[?1h'
else:
retdata += '\x1b[?1l'
retdata += b'\x1b[?1l'
return retdata, connstate
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)
except Exception:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
@@ -596,11 +634,14 @@ def disconnect_node(node, configmanager):
del _handled_consoles[consk]
def _nodechange(added, deleting, configmanager):
for node in added:
connect_node(node, configmanager)
def _nodechange(added, deleting, renamed, configmanager):
for node in deleting:
disconnect_node(node, configmanager)
for node in renamed:
disconnect_node(node, configmanager)
connect_node(renamed[node], configmanager)
for node in added:
connect_node(node, configmanager)
def _start_tenant_sessions(cfm):
@@ -624,17 +665,20 @@ def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager, username=None):
def connect_node(node, configmanager, username=None, direct=True, width=80,
height=24):
attrval = configmanager.get_node_attributes(node, 'collective.manager')
myc = attrval.get(node, {}).get('collective.manager', {}).get(
'value', None)
myname = collective.get_myname()
if myc and myc != collective.get_myname():
if myc and myc != collective.get_myname() and direct:
minfo = configmodule.get_collective_member(myc)
return ProxyConsole(node, minfo, myname, configmanager, username)
return ProxyConsole(node, minfo, myname, configmanager, username,
width, height)
consk = (node, configmanager.tenant)
if consk not in _handled_consoles:
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
_handled_consoles[consk] = ConsoleHandler(node, configmanager, width,
height)
return _handled_consoles[consk]
# A stub console handler that just passes through to a remote confluent
@@ -643,8 +687,10 @@ def connect_node(node, configmanager, username=None):
class ProxyConsole(object):
_genwatchattribs = frozenset(('collective.manager',))
def __init__(self, node, managerinfo, myname, configmanager, user):
def __init__(self, node, managerinfo, myname, configmanager, user, width,
height):
self.skipreplay = True
self.initsize = (width, height)
self.managerinfo = managerinfo
self.myname = myname
self.cfm = configmanager
@@ -655,6 +701,7 @@ class ProxyConsole(object):
self._attribwatcher = configmanager.watch_attributes(
(self.node,), self._genwatchattribs, self._attribschanged)
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
if self.clisession:
self.clisession.detach()
@@ -696,6 +743,8 @@ class ProxyConsole(object):
'tenant': self.cfm.tenant,
'node': self.node,
'skipreplay': self.skipreplay,
'width': self.initsize[0],
'height': self.initsize[1],
#TODO(jjohnson2): declare myself as a proxy,
#facilitate redirect rather than relay on manager change
},
@@ -735,6 +784,10 @@ class ProxyConsole(object):
def reopen(self):
tlvdata.send(self.remote, {'operation': 'reopen'})
def resize(self, width, height):
tlvdata.send(self.remote, {'operation': 'resize', 'width': width,
'height': height})
# this represents some api view of a console handler. This handles things like
# holding the caller specific queue data, for example, when http api should be
@@ -757,10 +810,9 @@ class ConsoleSession(object):
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
connector = connect_node
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False):
skipreplay=False, direct=True, width=80, height=24):
self.registered = False
self.tenant = configmanager.tenant
if not configmanager.is_node(node):
@@ -768,6 +820,10 @@ class ConsoleSession(object):
self.username = username
self.node = node
self.configmanager = configmanager
self.direct = direct # true if client is directly connected versus
# relay
self.width = width
self.height = height
self.connect_session()
self.registered = True
self._evt = None
@@ -796,12 +852,16 @@ class ConsoleSession(object):
between console and shell.
"""
self.conshdl = connect_node(self.node, self.configmanager,
self.username)
self.username, self.direct, self.width,
self.height)
def send_break(self):
"""Send break to remote system
"""
self.conshdl.send_break()
def resize(self, width, height):
self.conshdl.resize(width, height)
def get_buffer_age(self):
"""Get the age in seconds of the buffered data
+221 -31
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2017 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@
import confluent
import confluent.alerts as alerts
import confluent.log as log
import confluent.tlvdata as tlvdata
import confluent.config.attributes as attrscheme
import confluent.config.configmanager as cfm
@@ -62,14 +63,16 @@ import itertools
import os
try:
import cPickle as pickle
pargs = {}
except ImportError:
import pickle
pargs = {'encoding': 'utf-8'}
import socket
import struct
import sys
pluginmap = {}
dispatch_plugins = (b'ipmi', u'ipmi')
dispatch_plugins = (b'ipmi', u'ipmi', b'redfish', u'redfish', b'tsmsol', u'tsmsol')
def seek_element(currplace, currkey):
@@ -85,7 +88,10 @@ def seek_element(currplace, currkey):
def nested_lookup(nestdict, key):
try:
return reduce(seek_element, key, nestdict)
currloc = nestdict
for currk in key:
currloc = seek_element(currloc, currk)
return currloc
except TypeError:
raise exc.NotFoundException("Invalid element requested")
@@ -105,6 +111,8 @@ def load_plugins():
for plugin in os.listdir(plugindir):
if plugin.startswith('.'):
continue
if '__pycache__' in plugin:
continue
(plugin, plugtype) = os.path.splitext(plugin)
if plugtype == '.sh':
pluginmap[plugin] = shellmodule.Plugin(
@@ -123,7 +131,7 @@ def load_plugins():
rootcollections = ['discovery/', 'events/', 'networking/',
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , 'users/', 'version']
class PluginRoute(object):
@@ -143,6 +151,7 @@ def _init_core():
# be enumerated in any collection
noderesources = {
'attributes': {
'rename': PluginRoute({'handler': 'attributes'}),
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
'expression': PluginRoute({'handler': 'attributes'}),
@@ -161,10 +170,22 @@ def _init_core():
'default': 'ipmi',
}),
},
'clear': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'users': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'licenses': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'save_licenses': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -195,12 +216,48 @@ def _init_core():
'default': 'ipmi',
}),
},
'extended': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'advanced': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'storage': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'arrays': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'disks': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'volumes': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
})
},
'system': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'advanced': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'clear': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
})
},
},
'_console': {
@@ -233,6 +290,10 @@ def _init_core():
'default': 'ipmi',
}),
},
'description': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'events': {
'hardware': {
'log': PluginRoute({
@@ -334,10 +395,18 @@ def _init_core():
},
},
'support': {
'servicedata': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
}
nodegroupresources = {
'attributes': {
'check': PluginRoute({'handler': 'attributes'}),
'rename': PluginRoute({'handler': 'attributes'}),
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
@@ -353,17 +422,37 @@ def create_user(inputdata, configmanager):
configmanager.create_user(username, attributemap=inputdata)
def create_usergroup(inputdata, configmanager):
try:
groupname = inputdata['name']
del inputdata['name']
except (KeyError, ValueError):
raise exc.InvalidArgumentException()
configmanager.create_usergroup(groupname)
def update_usergroup(groupname, attribmap, configmanager):
try:
configmanager.set_usergroup(groupname, attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
def update_user(name, attribmap, configmanager):
try:
configmanager.set_user(name, attribmap)
except ValueError:
raise exc.InvalidArgumentException()
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
def show_usergroup(groupname, configmanager):
groupinfo = configmanager.get_usergroup(groupname)
for attr in groupinfo:
yield msg.Attributes(kv={attr: groupinfo[attr]})
def show_user(name, configmanager):
userobj = configmanager.get_user(name)
rv = {}
for attr in attrscheme.user.iterkeys():
for attr in attrscheme.user:
rv[attr] = None
if attr == 'password':
if 'cryptpass' in userobj:
@@ -376,6 +465,10 @@ def show_user(name, configmanager):
rv[attr] = userobj[attr]
yield msg.Attributes(kv={attr: rv[attr]},
desc=attrscheme.user[attr]['description'])
if 'role' in userobj:
yield msg.Attributes(kv={'role': userobj['role']})
def stripnode(iterablersp, node):
@@ -408,6 +501,10 @@ def delete_user(user, configmanager):
configmanager.del_user(user)
yield msg.DeletedResource(user)
def delete_usergroup(usergroup, configmanager):
configmanager.del_usergroup(usergroup)
yield msg.DeletedResource(usergroup)
def delete_nodegroup_collection(collectionpath, configmanager):
if len(collectionpath) == 2: # just the nodegroup
@@ -567,6 +664,15 @@ class BadPlugin(object):
self.node, self.plugin + ' is not a supported plugin')
class BadCollective(object):
def __init__(self, node):
self.node = node
def error(self, *args, **kwargs):
yield msg.ConfluentNodeError(
self.node, 'collective mode is active, but collective.manager '
'is not set for this node')
def abbreviate_noderange(configmanager, inputdata, operation):
if operation != 'create':
raise exc.InvalidArgumentException('Must be a create with nodes in list')
@@ -583,7 +689,10 @@ def handle_dispatch(connection, cert, dispatch, peername):
cfm.get_collective_member(peername)['fingerprint'], cert):
connection.close()
return
dispatch = pickle.loads(dispatch)
pversion = 0
if bytearray(dispatch)[0] == 0x80:
pversion = bytearray(dispatch)[1]
dispatch = pickle.loads(dispatch, **pargs)
configmanager = cfm.ConfigManager(dispatch['tenant'])
nodes = dispatch['nodes']
inputdata = dispatch['inputdata']
@@ -602,7 +711,7 @@ def handle_dispatch(connection, cert, dispatch, peername):
plugpath = nodeattr[node][attrname]['value']
elif 'default' in plugroute:
plugpath = plugroute['default']
if plugpath is not None:
if plugpath:
try:
hfunc = getattr(pluginmap[plugpath], operation)
except KeyError:
@@ -619,14 +728,18 @@ def handle_dispatch(connection, cert, dispatch, peername):
configmanager=configmanager,
inputdata=inputdata))
for res in itertools.chain(*passvalues):
_forward_rsp(connection, res)
_forward_rsp(connection, res, pversion)
except Exception as res:
_forward_rsp(connection, res)
_forward_rsp(connection, res, pversion)
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
def _forward_rsp(connection, res):
r = pickle.dumps(res)
def _forward_rsp(connection, res, pversion):
try:
r = pickle.dumps(res, protocol=pversion)
except TypeError:
r = pickle.dumps(Exception(
'Cannot serialize error, check collective.manager error logs for details' + str(res)), protocol=pversion)
rlen = len(r)
if not rlen:
return
@@ -636,6 +749,8 @@ def _forward_rsp(connection, res):
def handle_node_request(configmanager, inputdata, operation,
pathcomponents, autostrip=True):
if log.logfull:
raise exc.TargetResourceUnavailable('Filesystem full, free up space and restart confluent service')
iscollection = False
routespec = None
if pathcomponents[0] == 'noderange':
@@ -730,7 +845,7 @@ def handle_node_request(configmanager, inputdata, operation,
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
return passvalue
return [passvalue]
else:
return stripnode(passvalue, nodes[0])
elif 'pluginattrs' in plugroute:
@@ -759,7 +874,8 @@ def handle_node_request(configmanager, inputdata, operation,
continue
elif list(cfm.list_collective()):
badcollnodes.append(node)
if plugpath is not None:
continue
if plugpath:
try:
hfunc = getattr(pluginmap[plugpath], operation)
except KeyError:
@@ -769,11 +885,8 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
if badcollnodes:
raise exc.ConfluentException(
'collective management active, '
'collective.manager must be set for {0}'.format(
','.join(badcollnodes)))
for bn in badcollnodes:
nodesbyhandler[BadCollective(bn).error] = [bn]
workers = greenpool.GreenPool()
numworkers = 0
for hfunc in nodesbyhandler:
@@ -809,6 +922,8 @@ def iterate_queue(numworkers, passvalues, strip=False):
if nv == 'theend':
completions += 1
else:
if isinstance(nv, Exception):
raise nv
if strip and not isinstance(nv, console.Console):
nv.strip_node(strip)
yield nv
@@ -822,6 +937,8 @@ def addtoqueue(theq, fun, kwargs):
else:
for pv in result:
theq.put(pv)
except Exception as e:
theq.put(e)
finally:
theq.put('theend')
@@ -831,42 +948,90 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
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')
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} is unreachable'.format(a['name']))
if a:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} is unreachable'.format(
a['name']))
else:
yield msg.ConfluentResourceUnavailable(
node,
'"{0}" is not recognized as a collective member'.format(
manager))
return
if not util.cert_matches(a['fingerprint'], remote.getpeercert(
binary_form=True)):
raise Exception("Invalid certificate on peer")
tlvdata.recv(remote)
banner = tlvdata.recv(remote)
vers = banner.split()[2]
if vers == b'v0':
pvers = 2
elif vers == b'v1':
pvers = 4
if sys.version_info[0] < 3:
pvers = 2
tlvdata.recv(remote)
myname = collective.get_myname()
dreq = pickle.dumps({'name': myname, 'nodes': list(nodes),
'path': element,'tenant': configmanager.tenant,
'operation': operation, 'inputdata': inputdata})
'operation': operation, 'inputdata': inputdata},
protocol=pvers)
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote.sendall(dreq)
while True:
rlen = remote.recv(8)
try:
rlen = remote.recv(8)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rlen) < 8:
nlen = remote.recv(8 - len(rlen))
try:
nlen = remote.recv(8 - len(rlen))
except Exception:
nlen = 0
if not nlen:
raise Exception('Error receiving data')
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rlen += nlen
rlen = struct.unpack('!Q', rlen)[0]
if rlen == 0:
break
rsp = remote.recv(rlen)
try:
rsp = remote.recv(rlen)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rsp) < rlen:
nrsp = remote.recv(rlen - len(rsp))
try:
nrsp = remote.recv(rlen - len(rsp))
except Exception:
nrsp = 0
if not nrsp:
raise Exception('Error receving data')
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rsp += nrsp
rsp = pickle.loads(rsp)
try:
rsp = pickle.loads(rsp, **pargs)
except UnicodeDecodeError:
rsp = pickle.loads(rsp, encoding='latin1')
if isinstance(rsp, Exception):
raise rsp
yield rsp
@@ -912,6 +1077,31 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
configmanager, inputdata, operation, pathcomponents)
elif pathcomponents[0] == 'version':
return (msg.Attributes(kv={'version': confluent.__version__}),)
elif pathcomponents[0] == 'usergroups':
# TODO: when non-administrator accounts exist,
# they must only be allowed to see their own user
try:
usergroup = pathcomponents[1]
except IndexError: # it's just users/
if operation == 'create':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata,
configmanager=configmanager)
create_usergroup(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_usergroups(),
forcecollection=False)
if usergroup not in configmanager.list_usergroups():
raise exc.NotFoundException("Invalid usergroup %s" % usergroup)
if operation == 'retrieve':
return show_usergroup(usergroup, configmanager)
elif operation == 'delete':
return delete_usergroup(usergroup, configmanager)
elif operation == 'update':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata,
configmanager=configmanager)
update_usergroup(usergroup, inputdata.attribs, configmanager)
return show_usergroup(usergroup, configmanager)
elif pathcomponents[0] == 'users':
# TODO: when non-administrator accounts exist,
# they must only be allowed to see their own user
+82 -12
View File
@@ -63,10 +63,13 @@
import base64
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
import confluent.discovery.protocols.pxe as pxe
#import confluent.discovery.protocols.ssdp as ssdp
import confluent.discovery.protocols.slp as slp
import confluent.discovery.handlers.imm as imm
import confluent.discovery.handlers.cpstorage as cpstorage
import confluent.discovery.handlers.tsm as tsm
import confluent.discovery.handlers.pxe as pxeh
import confluent.discovery.handlers.smm as smm
import confluent.discovery.handlers.xcc as xcc
@@ -86,6 +89,13 @@ import eventlet.greenpool
import eventlet.semaphore
autosensors = set()
scanner = None
try:
unicode
except NameError:
unicode = str
class nesteddict(dict):
def __missing__(self, key):
@@ -98,6 +108,8 @@ nodehandlers = {
'service:management-hardware.IBM:integrated-management-module2': imm,
'pxe-client': pxeh,
'service:io-device.Lenovo:management-module': None,
'service:thinkagile-storage': cpstorage,
'service:lenovo-tsm': tsm,
}
servicenames = {
@@ -106,6 +118,8 @@ servicenames = {
'service:management-hardware.Lenovo:lenovo-xclarity-controller': 'lenovo-xcc',
'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2',
'service:io-device.Lenovo:management-module': 'lenovo-switch',
'service:thinkagile-storage': 'thinkagile-storagebmc',
'service:lenovo-tsm': 'lenovo-tsm',
}
servicebyname = {
@@ -114,6 +128,8 @@ servicebyname = {
'lenovo-xcc': 'service:management-hardware.Lenovo:lenovo-xclarity-controller',
'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2',
'lenovo-switch': 'service:io-device.Lenovo:management-module',
'thinkagile-storage': 'service:thinkagile-storagebmc',
'lenovo-tsm': 'service:lenovo-tsm',
}
discopool = eventlet.greenpool.GreenPool(500)
@@ -209,6 +225,8 @@ def send_discovery_datum(info):
if infotype in servicenames:
types.append(servicenames[infotype])
yield msg.KeyValueData({'types': types})
if 'otheraddresses' in info:
yield msg.KeyValueData({'otheripaddrs': list(info['otheraddresses'])})
def _info_matches(info, criteria):
@@ -358,7 +376,7 @@ def handle_autosense_config(operation, inputdata):
yield msg.KeyValueData({'enabled': autosense})
elif operation == 'update':
enabled = inputdata['enabled']
if type(enabled) in (unicode, str):
if type(enabled) in (unicode, bytes):
enabled = enabled.lower() in ('true', '1', 'y', 'yes', 'enable',
'enabled')
if autosense == enabled:
@@ -381,6 +399,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
raise exc.InvalidArgumentException()
rescan()
return (msg.KeyValueData({'rescan': 'started'}),)
elif operation in ('update', 'create'):
if 'node' not in inputdata:
raise exc.InvalidArgumentException('Missing node name in input')
@@ -391,8 +410,15 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
'Unable to {0} to {1}'.format(operation,
'/'.join(pathcomponents)))
handler = info['handler'].NodeHandler(info, configmanager)
eval_node(configmanager, handler, info, inputdata['node'],
manual=True)
try:
eval_node(configmanager, handler, info, inputdata['node'],
manual=True)
except Exception as e:
# or... incorrect passworod provided..
if 'Incorrect password' in str(e) or 'Unauthorized name' in str(e):
return [msg.ConfluentTargetInvalidCredentials(
inputdata['node'])]
raise
return [msg.AssignedResource(inputdata['node'])]
elif operation == 'delete':
mac = _get_mac_from_query(pathcomponents)
@@ -416,6 +442,8 @@ def handle_read_api_request(pathcomponents):
# TODO(jjohnson2): This should be more generalized...
# odd indexes into components are 'by-'*, even indexes
# starting at 2 are parameters to previous index
if pathcomponents == ['discovery', 'rescan']:
return (msg.KeyValueData({'scanning': bool(scanner)}),)
subcats, queryparms, indexof, coll = _parameterize_path(pathcomponents[1:])
if len(pathcomponents) == 1:
dirlist = [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
@@ -458,6 +486,9 @@ def _recheck_nodes_backend(nodeattribs, configmanager):
_map_unique_ids(nodeattribs)
# for the nodes whose attributes have changed, consider them as potential
# strangers
if nodeattribs:
macmap.vintage = 0 # expire current mac map data, in case
# the attributes changed impacted the result
for node in nodeattribs:
if node in known_nodes:
for somemac in known_nodes[node]:
@@ -571,6 +602,11 @@ def detected(info):
break
else: # no nodehandler, ignore for now
return
if (handler and not handler.NodeHandler.adequate(info) and
info.get('protocol', None)):
eventlet.spawn_after(10, info['protocol'].fix_info, info,
safe_detected)
return
try:
snum = info['attributes']['enclosure-serial-number'][0].strip()
if snum:
@@ -617,6 +653,9 @@ def detected(info):
uuid = info.get('uuid', None)
if uuid_is_valid(uuid):
known_uuids[uuid][info['hwaddr']] = info
info['otheraddresses'] = set([])
for i4addr in info.get('attributes', {}).get('ipv4-address', []):
info['otheraddresses'].add(i4addr)
if handler and handler.https_supported and not handler.https_cert:
if handler.cert_fail_reason == 'unreachable':
log.log(
@@ -671,7 +710,8 @@ def detected(info):
def b64tohex(b64str):
bd = base64.b64decode(b64str)
return ''.join(['{0:02x}'.format(ord(x)) for x in bd])
bd = bytearray(bd)
return ''.join(['{0:02x}'.format(x) for x in bd])
def get_enclosure_chain_head(nodename, cfg):
@@ -686,6 +726,10 @@ def get_enclosure_chain_head(nodename, cfg):
if ne in members:
raise exc.InvalidArgumentException(
'Circular chain that includes ' + nodename)
if not cfg.is_node(ne):
raise exc.InvalidArgumentException(
'{0} is chained to nonexistent node {1} '.format(
nodename, ne))
nodename = ne
members.append(nodename)
return nodename
@@ -851,7 +895,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
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()
handler.preconfig(nodename)
except Exception as e:
unknown_info[info['hwaddr']] = info
info['discostatus'] = 'unidentified'
@@ -905,9 +949,9 @@ def eval_node(cfg, handler, info, nodename, manual=False):
# raise exc.InvalidArgumentException(errorstr)
# log.log({'error': errorstr})
if encuuid in pending_by_uuid:
pending_by_uuid[encuuid].add(info)
pending_by_uuid[encuuid].append(info)
else:
pending_by_uuid[encuuid] = set([info])
pending_by_uuid[encuuid] = [info]
return
# We found the real smm, replace the list with the actual smm
# to continue
@@ -1026,6 +1070,14 @@ def discover_node(cfg, handler, info, nodename, manual):
traceback.print_exc()
return False
newnodeattribs = {}
if cfm.list_collective():
# We are in a collective, check collective.manager
cmc = cfg.get_node_attributes(nodename, 'collective.manager')
cm = cmc.get(nodename, {}).get('collective.manager', {}).get('value', None)
if not cm:
# Node is being discovered in collective, but no collective.manager, default
# to the collective member actually able to execute the discovery
newnodeattribs['collective.manager'] = collective.get_myname()
if 'uuid' in info:
newnodeattribs['id.uuid'] = info['uuid']
if 'serialnumber' in info:
@@ -1042,6 +1094,10 @@ def discover_node(cfg, handler, info, nodename, manual):
info['discostatus'] = 'discovered'
for i in pending_by_uuid.get(curruuid, []):
eventlet.spawn_n(_recheck_single_unknown_info, cfg, i)
try:
del pending_by_uuid[curruuid]
except KeyError:
pass
return True
log.log({'info': 'Detected {0}, but discovery.policy is not set to a '
'value allowing discovery (open or permissive)'.format(
@@ -1091,6 +1147,7 @@ needaddhandled = False
def _handle_nodelist_change(configmanager):
global needaddhandled
global nodeaddhandler
macmap.vintage = 0 # the current mac map is probably inaccurate
_recheck_nodes((), configmanager)
if needaddhandled:
needaddhandled = False
@@ -1099,10 +1156,18 @@ def _handle_nodelist_change(configmanager):
nodeaddhandler = None
def newnodes(added, deleting, configmanager):
def newnodes(added, deleting, renamed, configmanager):
global attribwatcher
global needaddhandled
global nodeaddhandler
alldeleting = set(deleting) | set(renamed)
for node in alldeleting:
if node not in known_nodes:
continue
for mac in known_nodes[node]:
if mac in known_info:
del known_info[mac]
del known_nodes[node]
_map_unique_ids()
configmanager.remove_watcher(attribwatcher)
allnodes = configmanager.list_nodes()
@@ -1142,12 +1207,17 @@ def _periodic_recheck(configmanager):
def rescan():
_map_unique_ids()
eventlet.spawn_n(slp.active_scan, safe_detected)
global scanner
if scanner:
return
else:
scanner = eventlet.spawn(slp.active_scan, safe_detected, slp)
def start_detection():
global attribwatcher
global rechecker
global rechecktime
_map_unique_ids()
cfg = cfm.ConfigManager(None)
allnodes = cfg.list_nodes()
@@ -1171,8 +1241,8 @@ def stop_autosense():
autosensors.discard(watcher)
def start_autosense():
autosensors.add(eventlet.spawn(slp.snoop, safe_detected))
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected))
autosensors.add(eventlet.spawn(slp.snoop, safe_detected, slp))
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected, pxe))
nodes_by_fprint = {}
@@ -1194,7 +1264,7 @@ def _map_unique_ids(nodes=None):
uuid_by_nodes = {}
fprint_by_nodes = {}
for uuid in nodes_by_uuid:
if not uuid_is_valid():
if not uuid_is_valid(uuid):
continue
node = nodes_by_uuid[uuid]
if node in bigmap:
@@ -26,16 +26,27 @@ ipmicommand.session.threading = eventlet.green.threading
ipmicommand.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
getaddrinfo = eventlet.support.greendns.getaddrinfo
DEFAULT_USER = 'USERID'
DEFAULT_PASS = 'PASSW0RD'
class NodeHandler(generic.NodeHandler):
DEFAULT_USER = 'USERID'
DEFAULT_PASS = 'PASSW0RD'
def _get_ipmicmd(self, user=DEFAULT_USER, password=DEFAULT_PASS):
return ipmicommand.Command(self.ipaddr, user, password)
def _get_ipmicmd(self, user=None, password=None):
priv = None
if user is None or password is None:
if self.trieddefault:
raise pygexc.IpmiException()
priv = 4 # manually indicate priv to avoid double-attempt
if user is None:
user = self.DEFAULT_USER
if password is None:
password = self.DEFAULT_PASS
return ipmicommand.Command(self.ipaddr, user, password,
privlevel=priv, keepalive=False)
def __init__(self, info, configmanager):
self.trieddefault = None
super(NodeHandler, self).__init__(info, configmanager)
def probe(self):
@@ -45,35 +56,37 @@ class NodeHandler(generic.NodeHandler):
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(self, nodename, reset=False):
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.
creds = self.configmanager.get_node_attributes(
nodename,
['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
user = creds.get(nodename, {}).get(
'secret.hardwaremanagementuser', {}).get('value', None)
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', None)
try:
ic = self._get_ipmicmd()
passwd = DEFAULT_PASS
passwd = self.DEFAULT_PASS
except pygexc.IpmiException as pi:
creds = self.configmanager.get_node_attributes(
nodename,
['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
user = creds.get(nodename, {}).get(
'secret.hardwaremanagementuser', {}).get('value', None)
havecustomcreds = False
if user is not None and user != DEFAULT_USER:
if user is not None and user != self.DEFAULT_USER:
havecustomcreds = True
else:
user = DEFAULT_USER
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', None)
if passwd is not None and passwd != DEFAULT_PASS:
user = self.DEFAULT_USER
if passwd is not None and passwd != self.DEFAULT_PASS:
havecustomcreds = True
else:
passwd = DEFAULT_PASS
passwd = self.DEFAULT_PASS
if havecustomcreds:
ic = 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,
@@ -93,45 +106,33 @@ class NodeHandler(generic.NodeHandler):
raise exc.TargetEndpointBadCredentials(
'secret.hardwaremanagementuser and/or '
'secret.hardwaremanagementpassword was not configured')
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newipinfo = 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)
plen = netconfig['prefix']
newip = '{0}/{1}'.format(newip, plen)
ic.set_net_configuration(ipv4_address=newip,
ipv4_configuration='static',
ipv4_gateway=netconfig['ipv4_gateway'])
elif self.ipaddr.startswith('fe80::'):
cfg.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
else:
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address')
newuser = cd['secret.hardwaremanagementuser']['value']
newpass = cd['secret.hardwaremanagementpassword']['value']
for uid in currusers:
if currusers[uid]['name'] == newuser:
# Use existing account that has been created
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)
if vc:
ic.register_key_handler(vc)
break
else:
newuserslot = lockedusers + 1
if newuserslot < 2:
newuserslot = 2
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
ic.set_user_name(newuserslot, newuser)
ic.set_user_access(newuserslot, lanchan,
privilege_level='administrator')
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
if havecustomcreds:
ic = self._get_ipmicmd(user, passwd)
if vc:
ic.register_key_handler(vc)
#We are remote operating on the account we are
#using, no need to try to set user access
#ic.set_user_access(newuserslot, lanchan,
# privilege_level='administrator')
# Now to zap others
for uid in currusers:
if uid != newuserslot:
@@ -152,6 +153,36 @@ class NodeHandler(generic.NodeHandler):
# name...
# the user will remain, but that is life
raise
if customconfig:
customconfig(ic)
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newipinfo = 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)
plen = netconfig['prefix']
newip = '{0}/{1}'.format(newip, plen)
currcfg = ic.get_net_configuration()
if currcfg['ipv4_address'] != newip:
# do not change the ipv4_config if the current config looks
# like it is already accurate
ic.set_net_configuration(ipv4_address=newip,
ipv4_configuration='static',
ipv4_gateway=netconfig[
'ipv4_gateway'])
elif self.ipaddr.startswith('fe80::'):
cfg.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
else:
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address')
if reset:
ic.reset_bmc()
return ic
@@ -0,0 +1,21 @@
# Copyright 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 confluent.discovery.handlers.bmc as bmchandler
class NodeHandler(bmchandler.NodeHandler):
DEFAULT_USER = 'admin'
DEFAULT_PASS = 'admin'
devname = 'BMC'
maxmacs = 2
@@ -40,6 +40,12 @@ class NodeHandler(object):
targsa = info['addresses'][0]
self.ipaddr = targsa[0]
@classmethod
def adequate(cls, info):
# Check if the referenced info is really enough, if false, a rescan
# may occur against the target in a short while
return True
def scan(self):
# Do completely passive things to enhance data.
# Probe is permitted to for example attempt a login
@@ -51,7 +57,7 @@ class NodeHandler(object):
# serial number and uuid to flesh out data as needed
return
def preconfig(self):
def preconfig(self, possiblenode):
return
def discoverable_by_switch(self, macs):
@@ -63,6 +69,27 @@ class NodeHandler(object):
self._fp = certificate
return True
def get_node_credentials(self, nodename, creds, defuser, defpass):
user = creds.get(nodename, {}).get(
'secret.hardwaremanagementuser', {}).get('value', None)
havecustomcreds = False
if user and not isinstance(user, str):
user = user.decode('utf8')
if user is not None and user != defuser:
havecustomcreds = True
else:
user = defuser
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', None)
if passwd and not isinstance(passwd, str):
passwd = passwd.decode('utf8')
if passwd is not None and passwd != defpass:
havecustomcreds = True
else:
passwd = defpass
return user, passwd, not havecustomcreds
@property
def cert_fail_reason(self):
if self._certfailreason == 1:
@@ -12,15 +12,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import confluent.discovery.handlers.bmc as bmchandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.private.util as pygutil
import string
import confluent.util as util
import struct
class NodeHandler(bmchandler.NodeHandler):
devname = 'IMM'
@classmethod
def adequate(cls, info):
# We can sometimes receive a partially initialized SLP packet
# This is not adequate for being satisfied
return bool(info.get('attributes', {}))
def scan(self):
slpattrs = self.info.get('attributes', {})
self.isdense = False
@@ -32,13 +39,14 @@ class NodeHandler(bmchandler.NodeHandler):
if wronguuid:
# we need to fix the first three portions of the uuid
uuidprefix = wronguuid.split('-')[:3]
uuidprefix = struct.pack(
'<IHH', *[int(x, 16) for x in uuidprefix]).encode('hex')
uuidprefix = codecs.encode(struct.pack(
'<IHH', *[int(x, 16) for x in uuidprefix]), 'hex')
uuidprefix = util.stringify(uuidprefix)
uuidprefix = uuidprefix[:8] + '-' + uuidprefix[8:12] + '-' + \
uuidprefix[12:16]
self.info['uuid'] = uuidprefix + '-' + '-'.join(
wronguuid.split('-')[3:])
self.info['uuid'] = string.lower(self.info['uuid'])
self.info['uuid'] = self.info['uuid'].lower()
if ff not in ('dense-computing', 'BC2'):
# do not probe unless it's a dense platform
return
@@ -12,21 +12,36 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import confluent.discovery.handlers.bmc as bmchandler
import confluent.exceptions as exc
import eventlet
webclient = eventlet.import_patched('pyghmi.util.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
def fixuuid(baduuid):
# SMM dumps it out in hex
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
a = struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]).encode(
a = codecs.encode(struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]),
'hex')
a = util.stringify(a)
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
return '-'.join(uuid).lower()
class NodeHandler(bmchandler.NodeHandler):
is_enclosure = True
devname = 'SMM'
maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain
maxmacs = 6 # support an enclosure, but try to avoid catching daisy chain
def scan(self):
# the UUID is in a weird order, fix it up to match
@@ -36,9 +51,151 @@ class NodeHandler(bmchandler.NodeHandler):
uuid = fixuuid(uuid[0])
self.info['uuid'] = uuid
def _validate_cert(self, certificate):
# Assumption is by the time we call config, that discovery core has
# vetted self._fp. Our job here then is just to make sure that
# the currect connection matches the previously saved cert
if not self._fp: # circumstances are that we haven't validated yet
self._fp = certificate
return certificate == self._fp
def _webconfigrules(self, wc):
rules = []
for rule in self.ruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
if name.lower() in ('expiry', 'expiration'):
rules.append('passwordDurationDays:' + value)
warndays = '5' if int(value) > 5 else value
rules.append('passwordExpireWarningDays:' + warndays)
if name.lower() in ('lockout', 'loginfailures'):
rules.append('passwordFailAllowdNum:' + value)
if name.lower() == 'reuse':
rules.append('passwordReuseCheckNum:' + value)
if rules:
apirequest = 'set={0}'.format(','.join(rules))
wc.request('POST', '/data', apirequest)
wc.getresponse().read()
def _webconfignet(self, wc, nodename):
cfg = self.configmanager
cd = cfg.get_node_attributes(
nodename, ['hardwaremanagement.manager'])
smmip = cd.get(nodename, {}).get('hardwaremanagement.manager', {}).get('value', None)
if smmip and ':' not in smmip:
smmip = getaddrinfo(smmip, 0)[0]
smmip = smmip[-1][0]
if smmip and ':' in smmip:
raise exc.NotImplementedException('IPv6 not supported')
netconfig = 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)
if gateway:
setdata += ',v4Gateway:{0}'.format(gateway)
wc.request('POST', '/data', setdata)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
if '<statusCode>0' not in rspdata:
raise Exception("Error configuring SMM Network")
return
if smmip and ':' in smmip and not smmip.startswith('fe80::'):
raise exc.NotImplementedException('IPv6 configuration TODO')
if self.ipaddr.startswith('fe80::'):
cfg.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
def _webconfigcreds(self, username, password):
wc = webclient.SecureHTTPConnection(self.ipaddr, 443, verifycallback=self._validate_cert)
wc.connect()
authdata = { # start by trying factory defaults
'user': 'USERID',
'password': 'PASSW0RD',
}
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/x-www-form-urlencoded'}
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
if 'authResult>0' not in rspdata:
# default credentials are refused, try with the actual
authdata['user'] = username
authdata['password'] = password
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
if 'renew_account' in rspdata:
raise Exception('Configured password has expired')
if 'authResult>0' not in rspdata:
raise Exception('Unknown username/password on SMM')
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
return wc
if 'renew_account' in rspdata:
passwdchange = {'oripwd': 'PASSW0RD', 'newpwd': password}
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
wc.request('POST', '/data/changepwd', urlencode(passwdchange))
rsp = wc.getresponse()
rspdata = rsp.read()
authdata['password'] = password
wc.request('POST', '/data/login', urlencode(authdata), headers)
rsp = wc.getresponse()
rspdata = util.stringify(rsp.read())
if 'authResult>0' in rspdata:
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
if username == 'USERID':
return wc
wc.request('POST', '/data', 'set=user(2,1,{0},511,,4,15,0)'.format(username))
rsp = wc.getresponse()
rspdata = rsp.read()
wc.request('POST', '/data/logout')
rsp = wc.getresponse()
rspdata = rsp.read()
authdata['user'] = username
wc.request('POST', '/data/login', urlencode(authdata, headers))
rsp = wc.getresponse()
rspdata = rsp.read()
tokens = fromstring(rspdata)
st2 = tokens.findall('st2')[0].text
wc.set_header('ST2', st2)
return wc
def config(self, nodename):
# SMM for now has to reset to assure configuration applies
super(NodeHandler, self).config(nodename)
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
creds = self.configmanager.get_node_attributes(
nodename,
['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
username = creds.get(nodename, {}).get(
'secret.hardwaremanagementuser', {}).get('value', 'USERID')
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', 'PASSW0RD')
if not isinstance(username, str):
username = username.decode('utf8')
if not isinstance(passwd, str):
passwd = passwd.decode('utf8')
if passwd == 'PASSW0RD' and self.ruleset:
raise Exception('Cannot support default password and setting password rules at same time')
if passwd == 'PASSW0RD':
# We must avoid hitting the web interface due to forced password change, best effert
self._bmcconfig(nodename)
else:
# Switch to full web based configuration, to mitigate risks with the SMM
wc = self._webconfigcreds(username, passwd)
self._webconfigrules(wc)
self._webconfignet(wc, nodename)
# notes for smm:
# POST to:
@@ -53,4 +210,4 @@ class NodeHandler(bmchandler.NodeHandler):
# with body user=USERID&password=Passw0rd!4321
# yields:
# <?xml version="1.0" encoding="UTF-8"?><root> <status>ok</status> <authResult>0</authResult> <forwardUrl>index.html</forwardUrl> </root>
# note forwardUrl, if password change needed, will indicate something else
# note forwardUrl, if password change needed, will indicate something else
@@ -0,0 +1,221 @@
# Copyright 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 confluent.discovery.handlers.generic as generic
import confluent.exceptions as exc
import confluent.netutil as netutil
import confluent.util as util
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')
class NodeHandler(generic.NodeHandler):
devname = 'TSM'
DEFAULT_USER = 'USERID'
DEFAULT_PASS = 'PASSW0RD'
def __init__(self, info, configmanager):
self.trieddefault = None
self.targuser = None
self.curruser = None
self.currpass = None
self.targpass = None
self.nodename = None
self.csrftok = None
self.channel = None
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/')
uuid = i.get('UUID', None)
if uuid:
self.info['uuid'] = uuid
def validate_cert(self, certificate):
# broadly speaking, merely checks consistency moment to moment,
# but if https_cert gets stricter, this check means something
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
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)
wc.set_header('Content-Type', 'application/json')
authmode = 0
if not self.trieddefault:
rsp, status = 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))
else:
authmode = 2
if status > 400:
rsp = util.stringify(rsp)
self.trieddefault = True
if '555' in rsp:
passchange = {
'Password': self.targpass,
'RetypePassword': self.targpass,
'param': 4,
'default_password': self.DEFAULT_PASS,
'username': self.DEFAULT_USER
}
if authmode == 2:
rsp, status = wc.grab_json_response_with_status('/api/reset-pass', passchange)
else:
rsp, status = 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)
else:
rsp, status = wc.grab_json_response_with_status('/api/session', urlencode(authdata))
self.csrftok = rsp['CSRFToken']
self.channel = rsp['channel']
self.curruser = self.DEFAULT_USER
self.currpass = self.targpass
return wc
else:
self.curruser = self.DEFAULT_USER
self.currpass = self.DEFAULT_PASS
self.csrftok = rsp['CSRFToken']
self.channel = rsp['channel']
return wc
if self.curruser:
authdata['username'] = self.curruser
authdata['password'] = self.currpass
if authmode != 1:
rsp, status = 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))
if status != 200:
return None
self.csrftok = rsp['CSRFToken']
self.channel = rsp['channel']
return wc
authdata['username'] = self.targuser
authdata['password'] = self.targpass
if authmode != 1:
rsp, status = 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))
if status != 200:
return None
self.curruser = self.targuser
self.currpass = self.targpass
self.csrftok = rsp['CSRFToken']
self.channel = rsp['channel']
return wc
def config(self, nodename):
self.nodename = nodename
creds = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
True)
cd = creds.get(nodename, {})
user, passwd, _ = self.get_node_credentials(
nodename, creds, self.DEFAULT_USER, self.DEFAULT_PASS)
user = util.stringify(user)
passwd = util.stringify(passwd)
self.targuser = user
self.targpass = passwd
wc = self._get_wc()
wc.set_header('X-CSRFTOKEN', self.csrftok)
curruserinfo = {}
authupdate = False
wc.set_header('Content-Type', 'application/json')
if user != self.curruser:
authupdate = True
if not curruserinfo:
curruserinfo = wc.grab_json_response('/api/settings/users')
authchg = curruserinfo[1]
authchg['name'] = user
if passwd != self.currpass:
authupdate = True
if not curruserinfo:
curruserinfo = wc.grab_json_response('/api/settings/users')
authchg = curruserinfo[1]
authchg['changepassword'] = 0
authchg['password_size'] = 'bytes_20'
authchg['password'] = passwd
authchg['confirm_password'] = passwd
if authupdate:
rsp, status = wc.grab_json_response_with_status('/api/settings/users/2', authchg, method='PUT')
if (cd.get('hardwaremanagement.method', {}).get('value', 'ipmi') != 'redfish'
or cd.get('console.method', {}).get('value', None) == 'ipmi'):
# IPMI must be enabled per user config
wc.grab_json_response('/api/settings/ipmilanconfig', {
'ipv4_enable': 1, 'ipv6_enable': 1,
'uncheckedipv4lanEnable': 0, 'uncheckedipv6lanEnable': 0,
'checkedipv4lanEnable': 1, 'checkedipv6lanEnable': 1})
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newipinfo = getaddrinfo(newip, 0)[0]
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
currnet = wc.grab_json_response('/api/settings/network')
for net in currnet:
if net['channel_number'] == self.channel and net['lan_enable'] == 0:
# ignore false indication and switch to 8 (dedicated)
self.channel = 8
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)
newmask = netutil.cidr_to_mask(netconfig['prefix'])
net['ipv4_address'] = newip
net['ipv4_subnet'] = newmask
if netconfig['ipv4_gateway']:
net['ipv4_gateway'] = netconfig['ipv4_gateway']
net['ipv4_dhcp_enable'] = 0
rsp, status = wc.grab_json_response_with_status(
'/api/settings/network/{0}'.format(net['id']), net, method='PUT')
break
elif self.ipaddr.startswith('fe80::'):
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)')
rsp, status = wc.grab_json_response_with_status('/api/session', method='DELETE')
if __name__ == '__main__':
import confluent.config.configmanager as cfm
c = cfm.ConfigManager(None)
import sys
info = {'addresses': [[sys.argv[1]]] }
print(repr(info))
testr = NodeHandler(info, c)
testr.config(sys.argv[2])
@@ -1,4 +1,4 @@
# Copyright 2017 Lenovo
# Copyright 2017-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,51 +12,418 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import codecs
import confluent.discovery.handlers.imm as immhandler
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 pyghmi.ipmi.oem.lenovo.imm as imm
import eventlet.green.socket as socket
webclient = eventlet.import_patched('pyghmi.util.webclient')
import struct
getaddrinfo = eventlet.support.greendns.getaddrinfo
def fixup_uuid(uuidprop):
baduuid = ''.join(uuidprop.split())
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
a = codecs.encode(struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]), 'hex')
a = util.stringify(a)
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
return '-'.join(uuid).upper()
class NodeHandler(immhandler.NodeHandler):
devname = 'XCC'
def preconfig(self):
def __init__(self, info, configmanager):
self._wc = None
self.nodename = None
self.tmpnodename = None
self.tmppasswd = None
self._atdefaultcreds = True
self._needpasswordchange = True
self._currcreds = (None, None)
super(NodeHandler, self).__init__(info, configmanager)
@classmethod
def adequate(cls, info):
# We can sometimes receive a partially initialized SLP packet
# This is not adequate for being satisfied
return bool(info.get('attributes', {}))
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']):
# skip preconfig for non-SD530 servers
return
self.trieddefault = None # Reset state on a preconfig attempt
# attempt to enable SMM
#it's normal to get a 'not supported' (193) for systems without an SMM
# need to branch on 3.00+ firmware
currfirm = self.info.get('attributes', {}).get('firmware-image-info', [''])[0]
currfirm = currfirm.split(':')
if len(currfirm) > 1:
currfirm = float(currfirm[1])
disableipmi = False
if currfirm >= 3:
# IPMI is disabled and we need it, also we need to go to *some* password
wc = self.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')
wc.set_basic_credentials(self._currcreds[0], self._currcreds[1])
rsp = 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(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
ipmicmd = None
try:
ipmicmd = self._get_ipmicmd()
ipmicmd = self._get_ipmicmd(self._currcreds[0], self._currcreds[1])
ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,))
except pygexc.IpmiException as e:
if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and
'Incorrect password' not in str(e)):
'Incorrect password' not in str(e) and
str(e) != 'Session no longer connected'):
# raise an issue if anything other than to be expected
if disableipmi:
_, _ = 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(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': False}}, method='PATCH')
#TODO: decide how to clean out if important
#as it stands, this can step on itself
#if ipmicmd:
# ipmicmd.ipmi_session.logout()
def validate_cert(self, certificate):
# broadly speaking, merely checks consistency moment to moment,
# but if https_cert gets stricter, this check means something
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
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)
})
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
if rsp.status != 200 and password == 'PASSW0RD':
rsp.read()
adata = json.dumps({
'username': username,
'password': newpassword,
})
headers = {'Connection': 'keep-alive',
'Content-Type': 'application/json'}
wc.request('POST', '/api/login', adata, headers)
rsp = wc.getresponse()
if rsp.status == 200:
pwdchanged = True
password = newpassword
else:
rsp.read()
return (None, None)
if rsp.status == 200:
self._currcreds = (username, password)
wc.set_basic_credentials(username, password)
rspdata = json.loads(rsp.read())
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'])
if rspdata.get('pwchg_required', None) == 'true':
wc.request('POST', '/api/function', json.dumps(
{'USER_UserPassChange': '1,{0}'.format(newpassword)}))
rsp = wc.getresponse()
rsp.read()
if rsp.status != 200:
return (None, None)
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'])
if pwdchanged:
# Remove the minimum change interval, to allow sane
# password changes after provisional changes
self.set_password_policy('')
return (wc, pwdchanged)
return (None, None)
@property
def wc(self):
passwd = None
isdefault = True
if self._wc is None:
self._wc = webclient.SecureHTTPConnection(
self.ipaddr, 443, verifycallback=self.validate_cert)
self._wc.connect()
nodename = None
if self.nodename:
nodename = self.nodename
inpreconfig = False
elif self.tmpnodename:
nodename = None
inpreconfig = True
if self._currcreds[0] is not None:
wc, pwdchanged = self.get_webclient(self._currcreds[0], self._currcreds[1], None)
if wc:
return wc
if nodename:
creds = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
user, passwd, isdefault = self.get_node_credentials(
nodename, creds, 'USERID', 'PASSW0RD')
if not self.trieddefault:
if not passwd:
# So in preconfig context, we don't have admin permission to
# actually divulge anything to the target
# however the target *will* demand a new password... if it's currently
# PASSW0RD
# use TempW0rd42 to avoid divulging a real password on the line
# This is replacing one well known password (PASSW0RD) with another
# (TempW0rd42)
passwd = 'TempW0rd42'
wc, pwdchanged = self.get_webclient('USERID', 'PASSW0RD', passwd)
if wc:
if pwdchanged:
if inpreconfig:
self.tmppasswd = passwd
else:
self._needpasswordchange = False
return wc
self.trieddefault = True
if isdefault:
return
self._atdefaultcreds = False
if self.tmppasswd:
wc, _ = self.get_webclient('USERID', self.tmppasswd, passwd)
else:
wc, _ = self.get_webclient(user, passwd, None)
if wc:
return wc
def set_password_policy(self, strruleset):
ruleset = {'USER_GlobalMinPassChgInt': '0'}
for rule in strruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
if name.lower() in ('expiry', 'expiration'):
ruleset['USER_GlobalPassExpPeriod'] = value
if int(value) < 5:
ruleset['USER_GlobalPassExpWarningPeriod'] = value
if name.lower() in ('lockout', 'loginfailures'):
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
ruleset['USER_GlobalMaxLoginFailures'] = value
if name.lower() == 'complexity':
ruleset['USER_GlobalPassComplexRequired'] = value
if name.lower() == 'reuse':
ruleset['USER_GlobalMinPassReuseCycle'] = value
try:
wc = self.wc
wc.grab_json_response('/api/dataset', ruleset)
wc.grab_json_response('/api/providers/logout')
except Exception as e:
print(repr(e))
pass
def _get_next_userid(self, wc):
userinfo = 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 _setup_xcc_account(self, username, passwd, wc):
userinfo = wc.grab_json_response('/api/dataset/imm_users')
uid = None
for user in userinfo['items'][0]['users']:
if user['users_user_name'] == username:
uid = user['users_user_id']
break
else:
for user in userinfo['items'][0]['users']:
if user['users_user_name'] == 'USERID':
uid = user['users_user_id']
break
if not uid:
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',
{'USER_UserPassChange': '{0},{1}'.format(uid, passwd)})
if username != 'USERID':
wc.grab_json_response(
'/api/function',
{'USER_UserModify': '{0},{1},,1,4,0,0,0,0,,8,'.format(uid, username)})
self.tmppasswd = None
self._currcreds = (username, passwd)
def _convert_sha256account(self, user, passwd, wc):
# First check if the specified user is sha256...
userinfo = wc.grab_json_response('/api/dataset/imm_users')
curruser = None
uid = None
user = util.stringify(user)
passwd = util.stringify(passwd)
for userent in userinfo['items'][0]['users']:
if userent['users_user_name'] == user:
curruser = userent
break
if curruser.get('users_pass_is_sha256', 0):
self._wc = None
wc = self.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)
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({
'username': '6pmu0ezczzcp',
'password': tpass,
})
headers = {'Connection': 'keep-alive', 'Content-Type': 'application/json'}
nwc.request('POST', '/api/login', adata, headers)
rsp = nwc.getresponse()
if rsp.status == 200:
rspdata = json.loads(rsp.read())
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'])
if rspdata.get('reason', False):
newpass = base64.b64encode(os.urandom(9)) + 'q4J$'
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)})
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)
if not pwdchanged:
nwc.grab_json_response(
'/api/function',
{'USER_UserPassChange': '{0},{1}'.format(curruser['users_user_id'], passwd)})
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')
def config(self, nodename, reset=False):
self.nodename = nodename
# 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.
ic = self._bmcconfig(nodename)
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
strruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
wc = self.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)
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)
self._convert_sha256account(user, passwd, wc)
cd = self.configmanager.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager', 'hardwaremanagement.method', 'console.method'],
True)
cd = cd.get(nodename, {})
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')
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(
'/redfish/v1/Managers/1/NetworkProtocol',
{'IPMI': {'ProtocolEnabled': True}}, method='PATCH')
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newipinfo = getaddrinfo(newip, 0)[0]
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
netconfig = netutil.get_nic_config(self.configmanager, nodename, ip=newip)
newmask = netutil.cidr_to_mask(netconfig['prefix'])
currinfo = wc.grab_json_response('/api/providers/logoninfo')
currip = currinfo.get('items', [{}])[0].get('ipv4_address', '')
# do not change the ipv4_config if the current config looks right already
if currip != newip:
statargs = {
'ENET_IPv4Ena': '1', 'ENET_IPv4AddrSource': '0',
'ENET_IPv4StaticIPAddr': newip, 'ENET_IPv4StaticIPNetMask': newmask
}
if netconfig['ipv4_gateway']:
statargs['ENET_IPv4GatewayIPAddr'] = netconfig['ipv4_gateway']
wc.grab_json_response('/api/dataset', statargs)
elif self.ipaddr.startswith('fe80::'):
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)')
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
# Ok, we can get the enclosure uuid now..
ic.oem_init()
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
enclosureuuid = self.info.get('attributes', {}).get('chassis-uuid', [None])[0]
if enclosureuuid:
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
enclosureuuid = enclosureuuid.lower()
em = self.configmanager.get_node_attributes(nodename,
'enclosure.manager')
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
@@ -65,8 +432,3 @@ class NodeHandler(immhandler.NodeHandler):
if em:
self.configmanager.set_node_attributes(
{em: {'id.uuid': enclosureuuid}})
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
# def config(self, nodename):
# return
@@ -34,8 +34,8 @@ pxearchs = {
def decode_uuid(rawguid):
lebytes = struct.unpack_from('<IHH', buffer(rawguid[:8]))
bebytes = struct.unpack_from('>HHI', buffer(rawguid[8:]))
lebytes = struct.unpack_from('<IHH', rawguid[:8])
bebytes = struct.unpack_from('>HHI', rawguid[8:])
return '{0:08X}-{1:04X}-{2:04X}-{3:04X}-{4:04X}{5:08X}'.format(
lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2]).lower()
@@ -72,7 +72,7 @@ def find_info_in_options(rq, optidx):
return uuid, arch
return uuid, arch
def snoop(handler):
def snoop(handler, protocol=None):
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
#prominent
#TODO(jjohnson2): IP_PKTINFO, recvmsg to get the destination ip, per
@@ -98,7 +98,7 @@ def snoop(handler):
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
optidx = 0
try:
optidx = rq.index('\x63\x82\x53\x63') + 4
optidx = rq.index(b'\x63\x82\x53\x63') + 4
except ValueError:
continue
uuid, arch = find_info_in_options(rq, optidx)
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
# Copyright 2017-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,6 +27,8 @@ import traceback
_slp_services = set([
'service:management-hardware.IBM:integrated-management-module2',
'service:lenovo-smm',
'service:ipmi',
'service:lighttpd',
'service:management-hardware.Lenovo:lenovo-xclarity-controller',
'service:management-hardware.IBM:chassis-management-module',
'service:management-hardware.Lenovo:chassis-management-module',
@@ -47,7 +49,6 @@ except AttributeError:
IPPROTO_IPV6 = 41 # Assume Windows value if socket is missing it
def _parse_slp_header(packet):
packet = bytearray(packet)
if len(packet) < 16 or packet[0] != 2:
@@ -83,6 +84,8 @@ def _parse_SrvRply(parsed):
:return:
"""
payload = parsed['payload']
if len(payload) < 4:
return
ecode, ucount = struct.unpack('!HH', bytes(payload[0:4]))
if ecode:
parsed['errorcode'] = ecode
@@ -233,36 +236,42 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
def _grab_rsps(socks, rsps, interval, xidmap):
r, _, _ = select.select(socks, (), (), interval)
r = None
res = select.select(socks, (), (), interval)
if res:
r = res[0]
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
neighutil.refresh_neigh()
_parse_slp_packet(rsp, peer, rsps, xidmap)
r, _, _ = select.select(socks, (), (), interval)
res = select.select(socks, (), (), interval)
if not res:
r = None
else:
r = res[0]
def _parse_attrlist(attrstr):
attribs = {}
previousattrlen = None
attrstr = util.stringify(attrstr)
while attrstr:
if len(attrstr) == previousattrlen:
raise Exception('Looping in attrstr parsing')
previousattrlen = len(attrstr)
if attrstr[0] == '(':
if ')' not in attrstr:
attribs['INCOMPLETE'] = True
return attribs
currattr = attrstr[1:attrstr.index(')')]
if '=' not in currattr: # Not allegedly kosher, but still..
currattr = currattr.decode('utf-8')
attribs[currattr] = None
else:
attrname, attrval = currattr.split('=')
attrname = attrname.decode('utf-8')
attrname, attrval = currattr.split('=', 1)
attribs[attrname] = []
for val in attrval.split(','):
try:
val = val.decode('utf-8')
except UnicodeDecodeError:
val = '*DECODEERROR*'
if val[:3] == '\\FF': # we should make this bytes
finalval = bytearray([])
for bnum in attrval[3:].split('\\'):
@@ -272,9 +281,9 @@ def _parse_attrlist(attrstr):
val = finalval
if 'uuid' in attrname and len(val) == 16:
lebytes = struct.unpack_from(
'<IHH', buffer(val[:8]))
'<IHH', memoryview(val[:8]))
bebytes = struct.unpack_from(
'>HHI', buffer(val[8:]))
'>HHI', memoryview(val[8:]))
val = '{0:08X}-{1:04X}-{2:04X}-{3:04X}-' \
'{4:04X}{5:08X}'.format(
lebytes[0], lebytes[1], lebytes[2], bebytes[0],
@@ -282,7 +291,7 @@ def _parse_attrlist(attrstr):
).lower()
attribs[attrname].append(val)
attrstr = attrstr[attrstr.index(')'):]
elif attrstr[0] == ',':
elif attrstr[0] == ','[0]:
attrstr = attrstr[1:]
elif ',' in attrstr:
currattr = attrstr[:attrstr.index(',')]
@@ -295,9 +304,11 @@ def _parse_attrlist(attrstr):
return attribs
def _parse_attrs(data, parsed):
def _parse_attrs(data, parsed, xid=None):
headinfo = _parse_slp_header(data)
if headinfo['function'] != 7 or headinfo['xid'] != parsed['xid']:
if xid is None:
xid = parsed['xid']
if headinfo['function'] != 7 or headinfo['xid'] != xid:
return
payload = headinfo['payload']
if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
@@ -307,8 +318,18 @@ def _parse_attrs(data, parsed):
parsed['attributes'] = _parse_attrlist(attrstr)
def fix_info(info, handler):
if '_attempts' not in info:
info['_attempts'] = 10
if info['_attempts'] == 0:
return
info['_attempts'] -= 1
_add_attributes(info)
handler(info)
def _add_attributes(parsed):
attrq = _generate_attr_request(parsed['services'][0], parsed['xid'])
xid = parsed.get('xid', 42)
attrq = _generate_attr_request(parsed['services'][0], xid)
target = None
# prefer reaching out to an fe80 if present, to be highly robust
# in face of network changes
@@ -323,13 +344,14 @@ def _add_attributes(parsed):
else:
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
net.settimeout(1.0)
net.connect(target)
except socket.error:
return
net.sendall(attrq)
rsp = net.recv(8192)
net.close()
_parse_attrs(rsp, parsed)
_parse_attrs(rsp, parsed, xid)
def query_srvtypes(target):
@@ -351,6 +373,7 @@ def query_srvtypes(target):
while tries and not connected:
tries -= 1
try:
net.settimeout(1.0)
net.connect(target)
connected = True
except socket.error:
@@ -383,7 +406,7 @@ def rescan(handler):
handler(scanned)
def snoop(handler):
def snoop(handler, protocol=None):
"""Watch for SLP activity
handler will be called with a dictionary of relevant attributes
@@ -392,7 +415,7 @@ def snoop(handler):
:return:
"""
tracelog = log.Logger('trace')
active_scan(handler)
active_scan(handler, protocol)
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')
@@ -469,13 +492,26 @@ def snoop(handler):
peerbymacaddress[mac]['xid'] = 1
_add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
peerbymacaddress[mac]['protocol'] = protocol
if 'service:ipmi' in peerbymacaddress[mac]['services']:
if 'service:ipmi//Athena:623' in peerbymacaddress[mac].get('urls', ()):
peerbymacaddress[mac]['services'] = ['service:thinkagile-storage']
else:
continue
if 'service:lightttpd' in peerbymacaddress[mac]['services']:
currinf = peerbymacaddress[mac]
curratt = currinf.get('attributes', {})
if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
peerbymacaddress[mac]['services'] = ['service:lenovo-tsm']
else:
continue
handler(peerbymacaddress[mac])
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def active_scan(handler):
def active_scan(handler, protocol=None):
known_peers = set([])
for scanned in scan():
for addr in scanned['addresses']:
@@ -486,6 +522,7 @@ def active_scan(handler):
break
known_peers.add(addr)
else:
scanned['protocol'] = protocol
handler(scanned)
@@ -531,6 +568,11 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
_grab_rsps((net, net4), rsps, 1, xidmap)
# now to analyze and flesh out the responses
for id in rsps:
if 'service:ipmi' in rsps[id]['services']:
if 'service:ipmi://Athena:623' in rsps[id].get('urls', ''):
rsps[id]['services'] = ['service:thinkagile-storage']
else:
continue
if localonly:
for addr in rsps[id]['addresses']:
if 'fe80' in addr[0]:
@@ -538,6 +580,15 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
else:
continue
_add_attributes(rsps[id])
if 'service:lighttpd' in rsps[id]['services']:
currinf = rsps[id]
curratt = currinf.get('attributes', {})
if curratt.get('System-Manufacturing', [None])[0] == 'Lenovo' and curratt.get('type', [None])[0] == 'LenovoThinkServer':
currinf['services'] = ['service:lenovo-tsm']
curratt['enclosure-serial-number'] = curratt['Product-Serial']
curratt['enclosure-machinetype-model'] = curratt['Machine-Type']
else:
continue
del rsps[id]['payload']
del rsps[id]['function']
del rsps[id]['xid']
@@ -78,8 +78,13 @@ def snoop(handler, byehandler=None):
for i4 in util.list_ips():
ssdp4mcast = socket.inet_pton(socket.AF_INET, mcastv4addr) + \
socket.inet_aton(i4['addr'])
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
ssdp4mcast)
try:
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
ssdp4mcast)
except socket.error as e:
if e.errno != 98:
# errno 98 can happen if aliased, skip for now
raise
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.bind(('', 1900))
net6.bind(('', 1900))
@@ -167,6 +172,7 @@ def _find_service(service, target):
net4.sendto(smsg.format(bcast, service), (bcast, 1900))
# 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 = {}
while r:
@@ -174,7 +180,10 @@ def _find_service(service, target):
(rsp, peer) = s.recvfrom(9000)
neighutil.refresh_neigh()
_parse_ssdp(peer, rsp, peerdata)
r, _, _ = select.select((net4, net6), (), (), 4)
timeout = deadline - util.monotonic_time()
if timeout < 0:
timeout = 0
r, _, _ = select.select((net4, net6), (), (), timeout)
for nid in peerdata:
yield peerdata[nid]
@@ -194,6 +203,8 @@ def _parse_ssdp(peer, rsp, peerdata):
if code == '200':
if nid in peerdata:
peerdatum = peerdata[nid]
if peer not in peerdatum['peers']:
peerdatum['peers'].append(peer)
else:
peerdatum = {
'peers': [peer],
+5 -1
View File
@@ -106,11 +106,15 @@ class PubkeyInvalid(ConfluentException):
super(PubkeyInvalid, self).__init__(self, text)
self.fingerprint = fingerprint
self.attrname = attribname
self.message = text
certtxt = base64.b64encode(certificate)
if not isinstance(certtxt, str):
certtxt = certtxt.decode('utf8')
bodydata = {'message': text,
'event': event,
'fingerprint': fingerprint,
'fingerprintfield': attribname,
'certificate': base64.b64encode(certificate)}
'certificate': certtxt}
self.errorbody = json.dumps(bodydata)
def get_error_body(self):
+29 -4
View File
@@ -19,30 +19,45 @@
# the time comes
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages as msg
import eventlet
import os
import pwd
import socket
import traceback
updatesbytarget = {}
uploadsbytarget = {}
downloadsbytarget = {}
updatepool = eventlet.greenpool.GreenPool(256)
_tracelog = None
def execupdate(handler, filename, updateobj, type):
if not os.path.exists(filename):
def execupdate(handler, filename, updateobj, type, owner, node):
global _tracelog
if type != 'ffdc' and not os.path.exists(filename):
errstr = '{0} does not appear to exist on {1}'.format(
filename, socket.gethostname())
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
return
if type == 'ffdc' and os.path.isdir(filename):
filename += '/' + node
try:
if type == 'firmware':
completion = handler(filename, progress=updateobj.handle_progress,
bank=updateobj.bank)
else:
completion = handler(filename, progress=updateobj.handle_progress)
if type == 'ffdc' and completion:
filename = completion
completion = None
if completion is None:
completion = 'complete'
if owner:
pwent = pwd.getpwnam(owner)
os.chown(filename, pwent.pw_uid, pwent.pw_gid)
updateobj.handle_progress({'phase': completion, 'progress': 100.0})
except exc.PubkeyInvalid as pi:
errstr = 'Certificate mismatch detected, does not match value in ' \
@@ -50,23 +65,28 @@ def execupdate(handler, filename, updateobj, type):
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
except Exception as e:
if _tracelog is None:
_tracelog = log.Logger('trace')
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event, event=log.Events.stacktrace)
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': str(e)})
class Updater(object):
def __init__(self, node, handler, filename, tenant=None, name=None,
bank=None, type='firmware'):
bank=None, type='firmware', owner=None):
self.bank = bank
self.node = node
self.phase = 'initializing'
self.detail = ''
self.percent = 0.0
self.updateproc = updatepool.spawn(execupdate, handler, filename,
self, type)
self, type, owner, node)
if type == 'firmware':
myparty = updatesbytarget
elif type == 'mediaupload':
myparty = uploadsbytarget
elif type == 'ffdc':
myparty = downloadsbytarget
if (node, tenant) not in myparty:
myparty[(node, tenant)] = {}
if name is None:
@@ -96,6 +116,8 @@ def remove_updates(nodes, tenant, element, type='firmware'):
upid = element[-1]
if type == 'firmware':
myparty = updatesbytarget
elif type == 'ffdc':
myparty = downloadsbytarget
else:
myparty = uploadsbytarget
for node in nodes:
@@ -115,6 +137,9 @@ def list_updates(nodes, tenant, element, type='firmware'):
if type == 'mediaupload':
myparty = uploadsbytarget
verb = 'upload'
elif type == 'ffdc':
verb = 'download'
myparty = downloadsbytarget
else:
myparty = updatesbytarget
verb = 'update'
+2
View File
@@ -49,6 +49,7 @@ def forward_port(sock, target, clientip, sessionid):
continue
try:
client = socket.create_connection((target, 443))
client.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 1456)
except Exception:
conn.close()
continue
@@ -68,6 +69,7 @@ def forward_video():
try:
vidclient = socket.create_connection((vidtargetbypeer[cli[0]],
3900))
vidclient.setsockopt(socket.IPPROTO_TCP, socket.TCP_MAXSEG, 1456)
except Exception:
conn.close()
continue
+65 -36
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
# 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.
@@ -17,7 +17,10 @@
# This SCGI server provides a http wrap to confluent api
# It additionally manages httprequest console sessions
import base64
import Cookie
try:
import Cookie
except ModuleNotFoundError:
import http.cookies as Cookie
import confluent.auth as auth
import confluent.config.attributes as attribs
import confluent.consoleserver as consoleserver
@@ -39,7 +42,10 @@ import socket
import sys
import traceback
import time
import urlparse
try:
import urlparse
except ModuleNotFoundError:
import urllib.parse as urlparse
import eventlet.wsgi
#scgi = eventlet.import_patched('flup.server.scgi')
tlvdata = confluent.tlvdata
@@ -74,7 +80,7 @@ def group_creation_resources():
yield confluent.messages.ListAttributes(kv={'nodes': []},
desc='Nodes to add to the group'
).html() + '<br>\n'
for attr in sorted(attribs.node.iterkeys()):
for attr in sorted(attribs.node):
if attr == 'groups':
continue
if attr.startswith("secret."):
@@ -95,7 +101,7 @@ def group_creation_resources():
def node_creation_resources():
yield confluent.messages.Attributes(
kv={'name': None}, desc="Name of the node").html() + '<br>'
for attr in sorted(attribs.node.iterkeys()):
for attr in sorted(attribs.node):
if attr.startswith("secret."):
yield confluent.messages.CryptedAttributes(
kv={attr: None},
@@ -126,7 +132,7 @@ def user_creation_resources():
'description': (''),
},
}
for attr in sorted(credential.iterkeys()):
for attr in sorted(credential):
if attr == "password":
yield confluent.messages.CryptedAttributes(
kv={attr: None},
@@ -176,7 +182,7 @@ def _get_query_dict(env, reqbody, reqtype):
if reqbody is not None:
if "application/x-www-form-urlencoded" in reqtype:
pbody = urlparse.parse_qs(reqbody, True)
for ky in pbody.iterkeys():
for ky in pbody:
if len(pbody[ky]) > 1: # e.g. REST explorer
na = [i for i in pbody[ky] if i != '']
qdict[ky] = na
@@ -184,7 +190,7 @@ def _get_query_dict(env, reqbody, reqtype):
qdict[ky] = pbody[ky][0]
elif 'application/json' in reqtype:
pbody = json.loads(reqbody)
for key in pbody.iterkeys():
for key in pbody:
qdict[key] = pbody[key]
if 'restexplorerhonorkey' in qdict:
nqdict = {}
@@ -269,6 +275,9 @@ def _authorize_request(env, operation):
name = ''
sessionid = None
cookie = Cookie.SimpleCookie()
element = env['PATH_INFO']
if element.startswith('/sessions/current/'):
element = None
if 'HTTP_COOKIE' in env:
#attempt to use the cookie. If it matches
cc = RobustCookie()
@@ -290,7 +299,7 @@ def _authorize_request(env, operation):
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
name, element=element, operation=operation,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
if env['PATH_INFO'] == '/sessions/current/logout':
@@ -302,9 +311,11 @@ def _authorize_request(env, operation):
return {'code': 401}
return ('logout',)
name, passphrase = base64.b64decode(
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
authdata = auth.check_user_passphrase(name, passphrase, element=None)
if not authdata:
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(b':', 1)
authdata = auth.check_user_passphrase(name, passphrase, operation=operation, element=element)
if authdata is False:
return {'code': 403}
elif not authdata:
return {'code': 401}
sessid = util.randomstring(32)
while sessid in httpsessions:
@@ -314,14 +325,14 @@ def _authorize_request(env, operation):
'inflight': set([])}
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
httpsessions[sessid]['csrftoken'] = util.randomstring(32)
cookie['confluentsessionid'] = sessid
cookie['confluentsessionid'] = util.stringify(sessid)
cookie['confluentsessionid']['secure'] = 1
cookie['confluentsessionid']['httponly'] = 1
cookie['confluentsessionid']['path'] = '/'
skiplog = _should_skip_authlog(env)
if authdata:
auditmsg = {
'user': name,
'user': util.stringify(name),
'operation': operation,
'target': env['PATH_INFO'],
}
@@ -333,7 +344,7 @@ def _authorize_request(env, operation):
if authdata[3] is not None:
auditmsg['tenant'] = authdata[3]
authinfo['tenant'] = authdata[3]
auditmsg['user'] = authdata[2]
auditmsg['user'] = util.stringify(authdata[2])
if sessid is not None:
authinfo['sessionid'] = sessid
if not skiplog:
@@ -341,15 +352,10 @@ def _authorize_request(env, operation):
if 'csrftoken' in httpsessions[sessid]:
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
return authinfo
else:
elif authdata is None:
return {'code': 401}
# TODO(jbjohnso): actually evaluate the request for authorization
# In theory, the x509 or http auth stuff will get translated and then
# passed on to the core authorization function in an appropriate form
# expresses return in the form of http code
# 401 if there is no known identity
# 403 if valid identity, but no access
# going to run 200 just to get going for now
else:
return {'code': 403}
def _pick_mimetype(env):
@@ -384,11 +390,11 @@ def resourcehandler(env, start_response):
try:
for rsp in resourcehandler_backend(env, start_response):
yield rsp
except:
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
start_response('500 - Internal Server Error', [])
yield '500 - Internal Server Error'
start_response('500 - ' + str(e), [])
yield '500 - ' + str(e)
return
@@ -429,7 +435,7 @@ def resourcehandler_backend(env, start_response):
return
if authorized['code'] == 403:
start_response('403 Forbidden', badauth)
yield 'authorization failed'
yield 'Forbidden'
return
if authorized['code'] != 200:
raise Exception("Unrecognized code from auth engine")
@@ -446,6 +452,8 @@ def resourcehandler_backend(env, start_response):
httpsessions[authorized['sessionid']]['inflight'])):
pagecontent += rsp
start_response("200 OK", headers)
if not isinstance(pagecontent, bytes):
pagecontent = pagecontent.encode('utf-8')
yield pagecontent
return
except exc.ConfluentException as e:
@@ -469,6 +477,10 @@ def resourcehandler_backend(env, start_response):
funport = forwarder.get_port(targip, env['HTTP_X_FORWARDED_FOR'],
authorized['sessionid'])
host = env['HTTP_X_FORWARDED_HOST']
if ']' in host:
host = host.split(']')[0] + ']'
elif ':' in host:
host = host.rsplit(':', 1)[0]
url = 'https://{0}:{1}/'.format(host, funport)
start_response('302', [('Location', url)])
yield 'Our princess is in another castle!'
@@ -487,7 +499,7 @@ def resourcehandler_backend(env, start_response):
auditmsg = {
'operation': 'start',
'target': env['PATH_INFO'],
'user': authorized['username'],
'user': util.stringify(authorized['username']),
}
if 'tenant' in authorized:
auditmsg['tenant'] = authorized['tenant']
@@ -496,6 +508,8 @@ def resourcehandler_backend(env, start_response):
skipreplay = False
if 'skipreplay' in querydict and querydict['skipreplay']:
skipreplay = True
width = querydict.get('width', 80)
height = querydict.get('height', 24)
datacallback = None
async = None
if 'HTTP_CONFLUENTASYNCID' in env:
@@ -507,13 +521,13 @@ def resourcehandler_backend(env, start_response):
consession = shellserver.ShellSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
datacallback=datacallback, width=width, height=height
)
else:
consession = consoleserver.ConsoleSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
datacallback=datacallback, width=width, height=height
)
except exc.NotFoundException:
start_response("404 Not found", headers)
@@ -548,6 +562,9 @@ def resourcehandler_backend(env, start_response):
elif 'action' in querydict:
if querydict['action'] == 'break':
consolesessions[querydict['session']]['session'].send_break()
elif querydict['action'] == 'resize':
consolesessions[querydict['session']]['session'].resize(
width=querydict['width'], height=querydict['height'])
elif querydict['action'] == 'reopen':
consolesessions[querydict['session']]['session'].reopen()
else:
@@ -617,6 +634,7 @@ def resourcehandler_backend(env, start_response):
sessinfo = {'username': authorized['username']}
if 'authtoken' in authorized:
sessinfo['authtoken'] = authorized['authtoken']
tlvdata.unicode_dictvalues(sessinfo)
yield json.dumps(sessinfo)
return
resource = '.' + url[url.rindex('/'):]
@@ -638,6 +656,8 @@ def resourcehandler_backend(env, start_response):
for datum in _assemble_json(hdlr, resource, url, extension):
pagecontent += datum
start_response('200 OK', headers)
if not isinstance(pagecontent, bytes):
pagecontent = pagecontent.encode('utf-8')
yield pagecontent
except exc.ConfluentException as e:
if ((not isinstance(e, exc.LockedCredentials)) and
@@ -722,7 +742,7 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
for rsp in responses:
if isinstance(rsp, confluent.messages.LinkRelation):
haldata = rsp.raw()
for hk in haldata.iterkeys():
for hk in haldata:
if 'href' in haldata[hk]:
if isinstance(haldata[hk]['href'], int):
haldata[hk]['href'] = str(haldata[hk]['href'])
@@ -738,7 +758,7 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
links[hk] = haldata[hk]
else:
rsp = rsp.raw()
for dk in rsp.iterkeys():
for dk in rsp:
if dk in rspdata:
if isinstance(rspdata[dk], list):
if isinstance(rsp[dk], list):
@@ -757,8 +777,8 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
rspdata[dk] = rsp[dk]
rspdata["_links"] = links
tlvdata.unicode_dictvalues(rspdata)
yield json.dumps(
rspdata, sort_keys=True, indent=4, ensure_ascii=False).encode('utf-8')
yield util.stringify(json.dumps(
rspdata, sort_keys=True, indent=4, ensure_ascii=False).encode('utf-8'))
def serve(bind_host, bind_port):
@@ -784,8 +804,17 @@ def serve(bind_host, bind_port):
'Failed to open HTTP due to busy port, trying again in'
' a second\n')
eventlet.sleep(1)
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
debug=False, socket_timeout=60)
# TCP_FASTOPEN
try:
sock.setsockopt(socket.SOL_TCP, 23, 5)
except Exception:
pass # we gave it our best shot there
try:
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
debug=False, socket_timeout=60)
except TypeError:
# Older eventlet in place, skip arguments it does not understand
eventlet.wsgi.server(sock, resourcehandler, log=False, debug=False)
class HttpApi(object):
@@ -41,6 +41,11 @@ class Console(object):
the remote console"""
return
def resize(self, width, height):
"""This function is responsible for relaying resize request from
client terminal to remote console"""
return
def ping(self):
"""This function is a hint to the console plugin that now would be a
nice time to assess health of console connection. Plugins that see
+76 -38
View File
@@ -51,11 +51,15 @@
# - leading bit reserved, 0 for now
# - length of metadata record 7 bits
# - type of data referenced by this entry (one byte), currently:
# 0=text event, 1=json, 2=console data
# 0=text event, 1=json, 2=console data, 3=event
# - offset into the text log to begin (4 bytes)
# - length of data referenced by this entry (2 bytes)
# - UTC timestamp of this entry in seconds since epoch (unsigned 32 bit?)
# - CRC32 over the record
# - UTC timestamp of this entry in seconds since epoch (unsigned 32 bit)
# - Event type (per 'Events' class below)
# - Event data (per event, currently used by connect/disconnect to represent
# single or multiple connections by user and for 'appmode' and 'shiftin'
# status for console
# - 2 reserved bytes
# (a future extended version might include suport for Forward Secure Sealing
# or other fields)
@@ -72,7 +76,13 @@ import stat
import struct
import time
import traceback
try:
unicode
except NameError:
unicode = str
daemonized = False
logfull = False
try:
from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_SH
except ImportError:
@@ -150,21 +160,36 @@ class BaseRotatingHandler(object):
Output the record to the file, catering for rollover as described
in doRollover().
"""
rolling_type = self.shouldRollover(binrecord, textrecord)
if rolling_type:
flock(self.textfile, LOCK_UN)
return self.doRollover(rolling_type)
return None
global logfull
try:
rolling_type = self.shouldRollover(binrecord, textrecord)
if rolling_type:
flock(self.textfile, LOCK_UN)
return self.doRollover(rolling_type)
return None
except (IOError, OSError) as e:
if not daemonized:
raise
logfull = True
def emit(self, binrecord, textrecord):
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
if self.binfile is None:
self.binfile = open(self.binpath, mode='ab')
self.textfile.write(textrecord)
self.binfile.write(binrecord)
self.textfile.flush()
self.binfile.flush()
global logfull
try:
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
if self.binfile is None:
self.binfile = open(self.binpath, mode='ab')
if not isinstance(textrecord, bytes):
textrecord = textrecord.encode('utf-8')
self.textfile.write(textrecord)
self.binfile.write(binrecord)
self.textfile.flush()
self.binfile.flush()
except (IOError, OSError) as e:
if not daemonized:
raise
logfull = True
def get_textfile_offset(self, data_len):
if self.textfile is None:
@@ -561,28 +586,39 @@ class Logger(object):
textdate = time.strftime(
'%b %d %H:%M:%S ', time.localtime(tstamp))
flock(textfile, LOCK_EX)
offset = textfile.tell() + len(textdate)
datalen = len(data)
eventaux = entry[4]
if eventaux is None:
eventaux = 0
# metadata length is always 16 for this code at the moment
binrecord = struct.pack(
">BBIHIBBH", 16, ltype, offset, datalen, tstamp, evtdata,
eventaux, 0)
if self.isconsole:
if ltype == 2:
textrecord = data
try:
offset = textfile.tell() + len(textdate)
datalen = len(data)
eventaux = entry[4]
if eventaux is None:
eventaux = 0
# metadata length is always 16 for this code at the moment
binrecord = struct.pack(
">BBIHIBBH", 16, ltype, offset, datalen, tstamp, evtdata,
eventaux, 0)
if self.isconsole:
if ltype == 2:
textrecord = data
else:
if not isinstance(textdate, bytes):
textdate = textdate.encode('utf-8')
if not isinstance(data, bytes):
data = data.encode('utf-8')
textrecord = textdate + data + b']'
else:
textrecord = textdate + data + ']'
else:
textrecord = textdate + data
if not textrecord.endswith('\n'):
textrecord += '\n'
files = self.handler.try_emit(binrecord, textrecord)
textrecord = textdate + data
if not textrecord.endswith('\n'):
textrecord += '\n'
files = self.handler.try_emit(binrecord, textrecord)
except struct.error:
files = self.handler.doRollover(RollingTypes.size_rolling)
finally:
try:
flock(textfile, LOCK_UN)
except Exception:
pass
if not files:
self.handler.emit(binrecord, textrecord)
flock(textfile, LOCK_UN)
else:
# Log the rolling event at first, then log the last data
# which cause the rolling event.
@@ -716,7 +752,7 @@ class Logger(object):
pass
def log(self, logdata=None, ltype=None, event=0, eventdata=None):
if type(logdata) not in (str, unicode, dict):
if type(logdata) not in (bytes, unicode, dict):
raise Exception("Unsupported logdata")
if ltype is None:
if type(logdata) == dict:
@@ -753,15 +789,17 @@ globaleventlog = None
tracelog = None
def log(logdata=None, ltype=None, event=0, eventdata=None):
def log(logdata=None, ltype=None, event=0, eventdata=None, flush=False):
global globaleventlog
if globaleventlog is None:
globaleventlog = Logger('events')
globaleventlog.log(logdata, ltype, event, eventdata)
if flush:
globaleventlog.writedata()
def logtrace():
global tracelog
if tracelog is None:
tracelog = Logger('trace', buffered=False)
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
event=Events.stacktrace)

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