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

Compare commits

...

427 Commits

Author SHA1 Message Date
Jarrod Johnson 6ab176218e Add '..' to range operators
The perl-style range operator exists in some tools, we
might as well support that alongside the usual '-'
operator and ':' python style operator.
2020-07-28 08:08:35 -04:00
Jarrod Johnson 5ccfa36da6 Fix ssh disconnect handling in python 3
In python 3, '' will never match b''. Just
use the value as a boolean to catch either ''
or b''.
2020-07-15 10:34:23 -04:00
Jarrod Johnson 5b1e144d32 Fix python3 problem with backspace
In python3, a single element reference of a bytes
is an int, not equal to a b'' of the same length and
value. Change to use slices to have python2 and python3
act the same.
2020-07-14 09:13:37 -04:00
Jarrod Johnson 4722c3ec92 Check current SMM IP before pushing config
If the user requests the same ip address that
is already present, leave the network configuration
alone.
2020-06-18 09:23:30 -04:00
Jarrod Johnson d75867050c Fallback when pyte contains unknown colors
This allows nodeconsole to connect to Ubuntu
installer, for example.
2020-05-22 15:46:16 -04:00
Jarrod Johnson 213d440052 Fix certificate watch
In python3, it was not working.
If another file got added, it could go
with a busy loop.
2020-05-22 13:55:24 -04:00
Jarrod Johnson 0800290c8e Fix checking colleective status
In a few contexts, the result of list_collective
was used directly in a boolean context. As an iterator,
it is always true unless coerced into a potentially empty
list explicitly.
2020-05-21 08:47:07 -04:00
Jarrod Johnson c5c5b36536 Fix reverse noderange in python 3
python3 environments do not natively know the word 'unicode',
in such a case, make unicode an alias for 'str'.
2020-05-19 14:18:35 -04:00
Jarrod Johnson 87a7e65b42 Add missing dependencies to deb package
A number of python packages were missing.
2020-05-12 10:31:36 -04:00
Jarrod Johnson 51c09d844f Fix broken expressions
Fix mistake in the {} password fix.
2020-05-08 13:29:33 -04:00
Jarrod Johnson 2c4f8dfceb Fix backup/restore with python3
backup/restore with password
was having problems with python3
2020-05-07 16:27:00 -04:00
Jarrod Johnson 3f53c55a66 Add custom port to nodeshell
Permit use of an alternative
port in nodeshell.
2020-05-07 08:27:16 -04:00
Jarrod Johnson 598ec4a294 Change ubuntu package names 2020-04-28 11:29:41 -04:00
Jarrod Johnson 501ab64e18 Revert "Add a utility to select disks"
This reverts commit 2936c7e8fd.
2020-04-27 17:37:21 -04:00
Jarrod Johnson 2936c7e8fd Add a utility to select disks
Use python to enable a bit
more flexibility and still be
readable.
2020-04-24 09:40:55 -04:00
Jarrod Johnson 4f85ba2bff Fix nodeattrib set of password
This path happens in nodeattrib.
2020-04-22 09:46:38 -04:00
Jarrod Johnson 5232b7c9c4 Fix passwords with {} in them
The input handler erroneously
tried to make an expression out
of values that did not support
expressions.
2020-04-22 08:58:38 -04:00
Jarrod Johnson f964fd8ce1 Add some information to man page
If the user consults the man page for help after a
HOSTUNREACH, give some text.
2020-04-17 10:00:56 -04:00
Jarrod Johnson f97fd3105f Prevent GET from indicating a non-idempotent opreation
This could bypass CSRF protection in theory.
2020-04-16 12:08:47 -04:00
Jarrod Johnson bc03da47af Fix another python3 syntax problem
async can't even be a member of
a class, evidently.
2020-04-10 12:12:17 -04:00
Jarrod Johnson bd39171611 Fix another use of async name
For better python 3 compatibility,
stop using async as a variable name.
2020-04-10 12:09:27 -04:00
Jarrod Johnson ed050b37e1 Fix httpapi with python3
async is now particularly special,
rename variable to fix it
2020-04-10 11:58:45 -04:00
Jarrod Johnson 8d1d19d9a8 Fix nodelicense save with expansion
Client side checking will not suffice.  Move it server side.

Additionally ,fix ownership of downloaded files.
2020-04-09 08:20:55 -04:00
Jarrod Johnson 017f3fb372 Switch CP storage to SSDP from SLP
The SLP behavior on CP storage BMC is problematic.
Switch to SSDP to see if that provides more robust
behavior.
2020-04-07 11:32:52 -04:00
Jarrod Johnson 46518f890b Fix nodediscover assign
The feature request for -n was
implemented in a way that broke
nodediscover assign.
2020-04-06 14:50:24 -04:00
Jarrod Johnson 7e86a72872 Pass along unavailable info to client 2020-04-03 12:33:55 -04:00
Jarrod Johnson 2567503662 Handle both types of CP reply
The CP storage may reply with
one of two distinct forms.  Recognize
either and treat them the same.
2020-04-03 11:01:55 -04:00
Jarrod Johnson 6b56181a52 Fix attrib add code
It was failing due to automatic pathing in python3
2020-04-02 11:42:54 -04:00
Jarrod Johnson c18ce50138 Integrate attributes into man pages 2020-04-02 11:01:42 -04:00
Jarrod Johnson a0684520d8 Add documentation for some parameter default 2020-04-02 10:25:57 -04:00
Jarrod Johnson 374aa49016 Add man page for nodersync 2020-04-02 10:17:44 -04:00
Jarrod Johnson 0b95daa30d Add msgpack to explicit dependencies
This will pull in msgpack for debian derivatives.
2020-03-30 10:58:08 -04:00
Jarrod Johnson d33365195b Have nodediscover list filter by -n
It is a reasonable expectation and useful feature to provide.
2020-03-27 12:14:41 -04:00
Jarrod Johnson 3429173c27 nodeconsole changes to specifically target tmux
tmux needs more direction to avoid ambiguous results.
2020-03-27 10:57:14 -04:00
Jarrod Johnson f6c44922f8 Add support for forced password change
ThinkAgile CP storage BMC firmware now requires
a password change be navigated prior to operation.
2020-03-26 14:10:17 -04:00
Jarrod Johnson a86d962984 Fix missing pwd import
The pwd module was accidentally omitted, fix the mistake.
2020-03-13 11:04:16 -04:00
Jarrod Johnson 9ee29aabe1 Set certificate ownership properly
When creating certificate for collective, ensure that the certificate
is usable by confluent when running
as non-root.
2020-03-12 16:04:23 -04:00
Jarrod Johnson a413f321fe Fix console loss on server exit
Catch the new exception that
was used to make other commands
exit cleaner.
2020-03-11 13:58:37 -04:00
Jarrod Johnson f2bd796c2a Further clean up license error handling
Backup of nodelicense was not
consistently checked between
redfish and ipmi plugins.
2020-03-11 09:29:41 -04:00
Jarrod Johnson bf31c4872f Fix mistake in nodelicense save
It was incorrectly presenting unrecognized error infomation.
2020-03-11 09:22:16 -04:00
Jarrod Johnson 634e5a8944 Update gitignore 2020-03-02 13:15:09 -05:00
Jarrod Johnson 67e3530d16 Add group count to collate
Feature request to offer the
ability to count output groups
rather than actually show output
groups.
2020-03-02 11:29:28 -05:00
Jarrod Johnson 3c26beda1d Fix loss of web connectivity during XCC discovery
The password policy was incorrectly logging out in the
middle of the flow when a forced password change occurred.
Fix by externally managing the web session.
2020-02-26 10:00:10 -05:00
Jarrod Johnson e2d0e49fc7 Add HTTP boot architecture to pxe
This paves the way for future response to HTTP boot
2020-02-20 20:36:36 -05:00
Jarrod Johnson da5a34c2e4 Fix wheezy builds 2020-02-20 08:05:21 -05:00
Jarrod Johnson 3629cb8ee7 Fix spelling of cumulus 2020-02-19 16:53:35 -05:00
Jarrod Johnson 8233e0a5bd Merge branch 'master' of github.com:jjohnson42/confluent 2020-02-19 16:26:48 -05:00
Jarrod Johnson eae7b3bd80 Add discovery snoop for Cumulus ZTP
When a cumulus switch does ZTP, detect
in the discovery facility.
2020-02-19 16:26:33 -05:00
Jarrod Johnson 868367e052 Add sensing of ONIE switches
Have nodediscover show detected
ONIE install devices.
2020-02-19 15:20:45 -05:00
Jarrod Johnson 6289cfaac4 Fix nodeboot when used with -m
nodeboot was erroneously using sys.argv rather
than the processed args from optionparser.
2020-02-19 14:36:10 -05:00
Jarrod Johnson f6d4fef5e6 Improve error message for collective
When trying to not run as root, give a
better error message explaining the
situation more clearly.
2020-02-18 16:16:40 -05:00
Jarrod Johnson b1b7ec4d50 Add affluent plugin
Implementing Cumulus NOS
support through an agent called
'affluent'.
2020-02-18 14:23:57 -05:00
Jarrod Johnson c0cd6de4f7 Remove PrivateDevices from unit file
PrivateDevices breaks pam_unix, for some reason.  Remove this
protection.  We still have DevicePolicy closed and running as non-root,
so this should still be relatively safe.i
2020-02-13 11:42:21 -05:00
Jarrod Johnson 4437e81e04 Leverage unix_chkpwd
If doing PAM authentication, we
can setuid to the target user and then
pam_unix will use unix_chkpwd on
our behalf.

Problems with this working in the lab
was resolved by a yum reinstall pam,
so it was presumably due to messed up
setcap or similar experiments.
2020-02-13 10:37:15 -05:00
Jarrod Johnson 6a12af1242 Remove non-root for older distributions
Older systemd does not support capabilities.  For such a platform,
disable non-root mode.
2020-02-12 13:20:08 -05:00
Jarrod Johnson 9879a83a10 Fix mistake in the redfish access protection
It contained a syntax error.
2020-02-11 14:22:19 -05:00
Jarrod Johnson cce6b824de Merge branch 'master' of github.com:jjohnson42/confluent 2020-02-11 14:09:51 -05:00
Jarrod Johnson ce1cb952e8 Fix PAM authentication
It's tricky.  On Redhat platforms, we need the CAP_DAC_READ_SEARCH
capability.  Unfortunately this is one of the nicest capabilities to have.

For now add it to ambient set so that PAM can work on redhat platforms.
Mitigate this risk by safeguarding the license handling code, which
is the only known place that can read a file and send it to somewhere.

If we could drop the capability from effective set and add it back in when
needed, that would be nice, but that appears not to be possible.

Short of that, having a separate authentication process
running and dropping privilege would potentially work.
2020-02-11 14:09:22 -05:00
Jarrod Johnson c6812274e4 Fix media list through collective
The Media class was not
serializable by msgpack.  Fix this
and improve error messages in
future instances of this behavior.
2020-02-11 09:04:49 -05:00
Jarrod Johnson 7cd7068dd7 Remove stray developer output
Remove a developer repr from log
output.
2020-02-07 16:01:29 -05:00
Jarrod Johnson 48f0330568 Add affluent support to /networking
The /networking backend will now
check for affluent on the switches and
use it if possible for improved performance.
2020-02-07 15:57:33 -05:00
Jarrod Johnson 66e1d17d28 Have systemd manage confluent run dir
The run directory has to be created and owned by confluent,
or else things cannot start.
2020-02-06 13:45:46 -05:00
Jarrod Johnson 7480494432 Tighten up new PAM check
For one, remove the password cache cleaning, as it no longer is run.

For another, skip the fork if uid is already 0.

Finally, wrap the check in a try/finally to keep the privileged process
more certain in exiting.
2020-02-06 10:05:57 -05:00
Jarrod Johnson 49c00bfbb7 Become root to check a password
Running as non-root had broken PAM support.  Allow setuid so we
can assume root in one specific case.
2020-02-05 16:06:13 -05:00
Jarrod Johnson 201985dd0e Fix missing argument to rpc_set_user
Requests were unable to traverse
a collective.
2020-02-05 14:55:51 -05:00
Jarrod Johnson 1aee19997a Carry errors across msgpack
Messages that were formerly carried
as pickled exceptions are now sent
as generic strings over msgpack.
2020-02-04 10:16:48 -05:00
Jarrod Johnson 3bc366bef4 Fix mistake in the cert util 2020-02-03 15:37:20 -05:00
Jarrod Johnson 4c83a1a04e Fix typos
Previous commit had errors in
quotations.
2020-02-03 11:13:13 -05:00
Jarrod Johnson cfae28a869 Add error mesasges to help with non-root confluent
non-root confluent daemon will have a larger struggle
with permissions, try to help the user navigate that.
2020-02-03 10:13:26 -05:00
Jarrod Johnson 44e6a72847 Switch to using the defined service
For now, this makes no difference, but it is poor form,
probably.  Correct by referencing the variable
name.
2020-02-03 09:57:02 -05:00
Jarrod Johnson 006fdc8280 Merge branch 'master' of github.com:jjohnson42/confluent 2020-02-02 18:19:06 -05:00
Jarrod Johnson 895b5264f6 Fix incorrect pam service
pam was defaulting to use of 'login', but we want 'confluent' for the service.
2020-02-02 18:18:39 -05:00
Jarrod Johnson 0b577af1ca Fix ownership of confluent cache
It needs to be owned by the confluent user.
2020-01-31 11:48:34 -05:00
Jarrod Johnson ff0b1bba7f Fix rpm spec file
There was an ommision and a mistake.
2020-01-31 10:37:49 -05:00
Jarrod Johnson 0badd9e5b4 Migrate confluent installs to non-root
This will check for and repair uid 0 owned confluent directories.
2020-01-31 10:16:33 -05:00
Jarrod Johnson c02064f0a5 Add missing msgpack dependencies 2020-01-31 10:02:38 -05:00
Jarrod Johnson c1b82d8163 Protect confluent private data
This blocks use of private confluent data in commands like
nodelicense, nodefirmware, and nodemedia.
2020-01-31 10:00:35 -05:00
Jarrod Johnson 0d5fa7a98a Change confluent to run as non-root and harden systemd
This mitigates a great deal of risk compared to prior behavior.
2020-01-31 09:52:52 -05:00
Jarrod Johnson 968efe719a Add CAP_NET_BIND_SERVICE to unit file
This is preparing for running as non-root.

We need this capability to snoop SLP and PXE
2020-01-31 09:34:13 -05:00
Jarrod Johnson 7a63ca8759 Fix python3 problem with confetty
Under python3, there is no unicode.
2020-01-31 08:53:42 -05:00
Jarrod Johnson a24866c2df Fix exitcode for confetty noderange commands
The exitcode was not being set for noderange commands
where each node may independently raise errors.

Correct the oversight by catching each subelements errors.
2020-01-31 08:22:20 -05:00
Jarrod Johnson c666b11138 Add ability to foreground exec confluent
This allows easier debug and option for unit file
in systemd to run foreground if it makes sense.
2020-01-31 08:10:01 -05:00
Jarrod Johnson 22f6198f60 Fix nodebmcreset on bad noderange
This prevents confusing python stack when
a bad noderange is specified.
2020-01-30 14:35:58 -05:00
Jarrod Johnson c99d01dffc Fix indentation of date conversion
The conversion was not checking each element.
2020-01-29 17:08:00 -05:00
Jarrod Johnson 8d0028a1de Catch all for serialization errors
Rather than odd bool error, return something a
bit more precise.
2020-01-29 15:45:27 -05:00
Jarrod Johnson bb9c2297c9 Stringify firmware datetime
With the change to msgpack, datetime objects cannot be serialized.  Apply
tlvdata compliant transform before storing.
2020-01-29 15:41:13 -05:00
Jarrod Johnson 91fa5bd1eb Enhance nodeconfig treatment of IMM
This makes the IMM attributes usable, but not intrusive.
2020-01-29 14:20:56 -05:00
Jarrod Johnson ac9609c40d Adjust to pyghmi api change
Due to confusion of mixed settings, pyghmi api changes
to enable the confluent experience to be more
sane.
2020-01-29 10:56:31 -05:00
Jarrod Johnson 0c4cb49c20 Implement nodeconfig -e
This provides access to 'extra' settings.
Mainly intended to avoid slowing down nodeconfig
with IMM attributes that most people don't
want anyway.
2020-01-29 10:15:32 -05:00
Jarrod Johnson 4be4100014 Fix configmanager msgpack
msgpack method had some regressions.  For one, python2 strings
became bytes on mixed collective, fix by using raw=False on the
receiver.

Additionally, del_nodes tends to use sets, and that's not viable for
msgpack.  Guard against that.
2020-01-29 09:24:57 -05:00
Jarrod Johnson 9f7c8c69f2 Fix invalid credentials msgpack
The invalid credentials did not accept an argument like
the rest, fix the inconsistency for msgpack deserialization.
2020-01-28 15:41:50 -05:00
Jarrod Johnson c35f7d99f7 Update stripped exceptions to include node
While the exception had the node name, in some contexts the
exception was processed genericly.
2020-01-28 10:18:58 -05:00
Jarrod Johnson 445950d02a Roll back library level force of role
Doing collective and config restore breaks.  The API
will still prevent implicit role assumption.
2020-01-28 10:05:04 -05:00
Jarrod Johnson cf72cf2d8c Require role explicitly on user/group creation
Rather than default to administrator, require
the user to explicitly set the role to administrator.
2020-01-27 16:12:03 -05:00
Jarrod Johnson 0652a7321b Apply whitelist to rpc functions in configmanager 2020-01-27 15:59:22 -05:00
Jarrod Johnson 4c8ba92856 Change configuration sync to use msgpack
This removes use of pickle for config sync over network.
2020-01-27 15:53:29 -05:00
Jarrod Johnson 09582d7597 Move input handling to destination
It is tricky to serialize a configmanager object, and
probably was making the requests gigantic anyway.

Serialize the parameters to let the target use its local copy instead
of serializing an entire config manager.
2020-01-27 15:26:54 -05:00
Jarrod Johnson 8a9e9aa7b3 Always use string type in msgpack
To facilitate py2/py3 consistency, for these
messages just always use the native string.
With this, python 2 strings will be unpacked as
strings by python 3.  This means bytes cannot be
passed, but we will suffer that limitation for now.
2020-01-24 14:07:34 -05:00
Jarrod Johnson b766e7b0ee Opt into the msgpack 1.0 behavior
This fixes the dispatch to actually work.
2020-01-24 11:39:44 -05:00
Jarrod Johnson 92699e47f2 Merge tag '2.4.1'
2.4 bugfix release
2020-01-24 09:59:29 -05:00
Jarrod Johnson 2aa9910d83 Fix XCC discovery issues
A couple of issues could occur during discover that should be
stepped over.
2020-01-23 08:45:43 -05:00
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 604ebcde3b Merge branch 'master' of github.com:jjohnson42/confluent 2020-01-22 14:24:21 -05:00
Jarrod Johnson b4b733a573 Fix attribute sort with python 3
For attributes without a sortid, sort was failing with python3
2020-01-22 14:24:10 -05:00
Jarrod Johnson 3bf083deb3 Stage 3 of msgpack for dispatch
This may complete the dispatch portion of the msgpack migration.
2020-01-21 14:15:14 -05:00
Jarrod Johnson 9d770632ce Merge branch 'master' of github.com:jjohnson42/confluent 2020-01-21 13:44:04 -05:00
Jarrod Johnson 79afd174c9 Add serialization to ConfluentExceptions
In the same manner that messages
are handled, handle non-messages
content.
2020-01-21 11:28:08 -05:00
Jarrod Johnson 13a0bf4fbe Draft for message serialization
This adds msgpack based serialization to messages.

This would be used to superesed pickle in core.
2020-01-16 16:42:32 -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 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 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 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 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 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 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 7fecd0ac5c Add man page for collective command 2019-04-04 10:18:06 -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 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 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 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 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 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 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 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 9379c85d0e Fix nodegrouplist man page 2019-03-28 10:05:35 -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 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 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 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 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 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
124 changed files with 6348 additions and 939 deletions
+4 -1
View File
@@ -1 +1,4 @@
.idea
*.pyc
.*.
confluent_client/man/man*
.*.sw*
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/python
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
try:
sys.path.remove(path)
except Exception:
pass
path = os.path.realpath(os.path.join(path, '..', 'confluent_server'))
sys.path.append(path)
import confluent.config.attributes as attr
import shutil
shutil.copyfile('doc/man/nodeattrib.ronn.tmpl', 'doc/man/nodeattrib.ronn')
shutil.copyfile('doc/man/nodegroupattrib.ronn.tmpl', 'doc/man/nodegroupattrib.ronn')
with open('doc/man/nodeattrib.ronn', 'a') as outf:
for field in sorted(attr.node):
outf.write('\n* `{0}`:\n {1}\n'.format(field, attr.node[field]['description']))
with open('doc/man/nodegroupattrib.ronn', 'a') as outf:
for field in sorted(attr.node):
outf.write('\n* `{0}`:\n {1}\n'.format(field, attr.node[field]['description']))
+6 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -43,6 +43,8 @@ argparser.add_option('-d', '--diff', action='store_true',
'output group and others')
argparser.add_option('-w', '--watch', action='store_true',
help='Show intermediate results while running')
argparser.add_option('-g', '--groupcount', action='store_true',
help='Show count of output groups rather than the actual output')
argparser.add_option('-s', '--skipcommon', action='store_true',
help='Do not print most common result, only non modal '
'groups, useful when combined with -d')
@@ -69,6 +71,9 @@ def print_current():
if options.diff:
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
reverse=options.reverse, basenode=options.base)
elif options.groupcount:
grouped.generate_byoutput()
print(len(grouped.byoutput))
else:
grouped.print_all(skipmodal=options.skipcommon,
count=options.count,
+34 -12
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
@@ -76,6 +76,11 @@ import confluent.termhandler as termhandler
import confluent.tlvdata as tlvdata
import confluent.client as client
try:
unicode
except NameError:
unicode = str
conserversequence = '\x05c' # ctrl-e, c
clearpowermessage = False
@@ -89,6 +94,10 @@ except NameError:
netserver = None
laststate = {}
try:
input = raw_input
except NameError:
pass
class BailOut(Exception):
def __init__(self, errorcode=0):
@@ -195,9 +204,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")
@@ -295,8 +304,11 @@ currchildren = None
def print_result(res):
global exitcode
if 'errorcode' in res or 'error' in res:
print res['error']
print(res['error'])
if 'errorcode' in res:
exitcode |= res['errorcode']
return
if 'databynode' in res:
print_result(res['databynode'])
@@ -309,9 +321,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'])
@@ -324,7 +336,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
@@ -423,10 +435,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')
@@ -504,6 +516,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):
@@ -844,7 +863,7 @@ 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)
@@ -864,7 +883,7 @@ def main():
global inconsole
try:
server_connect()
except EOFError, KeyboardInterrupt:
except (EOFError, KeyboardInterrupt) as _:
raise BailOut(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
@@ -922,6 +941,7 @@ def main():
updatestatus(data)
continue
if data is not None:
data = client.stringify(data)
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
@@ -945,7 +965,7 @@ def main():
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
except Exception:
pass
if not connected:
time.sleep(1)
@@ -997,4 +1017,6 @@ if __name__ == '__main__':
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)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
import optparse
import signal
import sys
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
import csv
import optparse
import signal
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
import csv
import optparse
import signal
@@ -26,7 +26,7 @@ def lookupdata(data, key):
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o ansible.hosts
usage='''\n %prog noderange -o xcatnodes.def
\n ''')
argparser.add_option('-o', '--output',
help='xCAT stanza file')
+7 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -50,6 +50,9 @@ 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()
@@ -87,6 +90,7 @@ if len(args) > 1:
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
@@ -126,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)
+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,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,8 +45,10 @@ 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
if success != 0:
sys.exit(success)
# 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 + ": BMC Reset Successful"
print(node + ": BMC Reset Successful")
sys.exit(success)
+8 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -42,6 +42,10 @@ 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()
@@ -54,8 +58,8 @@ except IndexError:
sys.exit(1)
client.check_globbing(noderange)
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()
@@ -66,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,
+64 -17
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -54,6 +54,12 @@ argparser.add_option('-d', '--detail', dest='detail',
action='store_true', default=False,
help='Provide verbose information as available, such as '
'help text and possible valid values')
argparser.add_option('-e', '--extra', dest='extra',
action='store_true', default=False,
help='Access extra configuration. Extra configuration is generally '
'reserved for unpopular or redundant options that may be slow to '
'read. Notably the IMM category on Lenovo settings is considered '
'to be extra configuration')
argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
@@ -68,6 +74,10 @@ argparser.add_option('-r', '--restoredefault', default=False,
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 = {
@@ -98,6 +108,9 @@ setmode = None
assignment = {}
queryparms = {}
printsys = []
printbmc = []
printextbmc = []
printallbmc = False
setsys = {}
forceset = False
needval = None
@@ -122,7 +135,7 @@ def _assign_value():
def parse_config_line(arguments):
global setmode, forceset, key, value, needval, candidate, path, attrib
global setmode, printallbmc, forceset, key, value, needval, candidate, path, attrib
for param in arguments:
if param == 'show':
continue # forgive muscle memory of pasu users
@@ -154,6 +167,8 @@ def parse_config_line(arguments):
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)):
@@ -169,10 +184,19 @@ def parse_config_line(arguments):
del queryparms[path]
except KeyError:
pass
if not matchedparms:
if param.lower() == 'imm':
printextbmc.append(param)
options.extra = True
elif not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
printsys.append(param)
if param.startswith('bmc.'):
printbmc.append(param.replace('bmc.', ''))
elif param.lower().startswith('imm'):
options.extra = True
printextbmc.append(param)
else:
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
@@ -197,19 +221,29 @@ else:
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault and options.restoredefault.lower() in (
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:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
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)
@@ -217,8 +251,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:
@@ -250,6 +288,15 @@ else:
NullOpt(), queryparms[path])
if rc:
sys.exit(rc)
if printsys == 'all' or printextbmc or printbmc or printallbmc:
if printbmc or not printextbmc:
rcode = client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/all'.format(noderange),
session, printbmc, options, attrprefix='bmc.')
if options.extra:
rcode |= client.print_attrib_path(
'/noderange/{0}/configuration/management_controller/extended/extra'.format(noderange),
session, printextbmc, options)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
+12 -11
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
@@ -51,27 +51,28 @@ if options.tile:
nodes.append(node)
initial = True
pane = 0
sessname = 'nodeconsole_{0}'.format(os.getpid())
for node in sortutil.natural_sort(nodes):
panename = '{0}:{1}'.format(sessname, pane)
if initial:
initial = False
subprocess.call(
['tmux', 'new-session', '-d', '-s',
'nodeconsole_{0}'.format(os.getpid()), '-x', '800', '-y',
sessname, '-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', 'select-pane', '-t', sessname])
subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
subprocess.call(
['tmux', 'split', '-h',
['tmux', 'split', '-h', '-t', sessname,
'{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()))
subprocess.call(['tmux', 'select-layout', '-t', sessname, 'tiled'], stdout=null)
pane += 1
subprocess.call(['tmux', 'select-pane', '-t', sessname])
subprocess.call(['tmux', 'set-option', '-t', panename, 'pane-border-status', 'top'], stderr=null)
os.execlp('tmux', 'tmux', 'attach', '-t', sessname)
else:
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
+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
@@ -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)
+23 -12
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -92,6 +92,8 @@ def process_header(header):
fields.append('hardwaremanagement.manager')
elif datum in ('bmc gateway', 'xcc gateway', 'imm 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'):
@@ -152,6 +154,7 @@ def import_csv(options, session):
for field in fields:
if field in unique_fields:
unique_data[field] = set([])
broken = False
for record in records:
currfields = list(fields)
nodedatum = {}
@@ -168,14 +171,16 @@ def import_csv(options, session):
nodedatum[currfield] = datum
if not datum_complete(nodedatum):
sys.exit(1)
if not search_record(nodedatum, options, session):
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)
@@ -232,8 +237,12 @@ def clear_discovery(options, session):
else:
print(repr(res))
def list_matching_macs(options, session):
def list_matching_macs(options, session, node=None, checknode=True):
path = '/discovery/'
if node:
path += 'by-node/{0}/'.format(node)
elif checknode and options.node:
path += 'by-node/{0}/'.format(options.node)
if options.model:
path += 'by-model/{0}/'.format(options.model)
if options.serial:
@@ -256,25 +265,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, False)
if not matches:
# Do a rescan to catch missing requested data
blocking_scan(session)
matches = list_matching_macs(options, session)
matches = list_matching_macs(options, session, None if needid else options.node, False)
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
@@ -332,7 +341,7 @@ def main():
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()
@@ -342,6 +351,8 @@ 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':
blocking_scan(session)
print("Rescan complete")
+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)))
+8 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -59,6 +59,10 @@ argparser = optparse.OptionParser(
"%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:
@@ -95,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)
@@ -151,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()
@@ -163,4 +168,4 @@ try:
update_firmware(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(exitcode)
+4 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -97,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)
@@ -123,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)
+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
@@ -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)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
+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)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
+3 -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
@@ -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()
+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(
+14 -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
@@ -142,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)
+74 -13
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
@@ -36,21 +36,33 @@ exitcode = 0
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][install <file>]")
"%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]
else:
components = ['all']
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
resource = '/noderange/{0}/configuration/' \
@@ -58,27 +70,76 @@ def install_license(session, filename):
filename = os.path.abspath(filename)
instargs = {'filename': filename}
for res in session.create(resource, instargs):
pass # print(repr(res))
show_licenses()
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)
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))
elif 'error' in res['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(node, res['databynode'][node]['error']))
else:
sys.stderr.write('{0}: {1}\n'.format(node, repr(res['databynode'][node])))
def show_licenses(session):
global exitcode
firmware_shown = False
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', []):
print('{0}: {1}'.format(node, license.get('feature',
'Unknown')))
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 is None:
show_licenses(session)
else:
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)
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)
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
+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)
+13 -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
@@ -42,6 +42,10 @@ 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
@@ -55,7 +59,7 @@ def run():
noderange, targpath = args[-1].split(':', 1)
client.check_globbing(noderange)
c = client.Command()
cmdstr = " ".join(args[:-1])
cmdstr = ' '.join(args[:-1])
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
cmdstr += ' {node}:' + targpath
@@ -64,15 +68,17 @@ def run():
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}):
{'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'].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
@@ -98,7 +104,7 @@ def run():
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = ''
pernodeout[node] += data
pernodeout[node] += client.stringify(data)
if '\n' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
if currout:
@@ -117,7 +123,7 @@ def run():
output.set_output(node, 'error!')
if node not in nodeerrs:
nodeerrs[node] = ''
nodeerrs[node] += data
nodeerrs[node] += client.stringify(data)
else:
pop = desc['popen']
ret = pop.poll()
+11 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -46,6 +46,10 @@ def run():
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()
@@ -63,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:
@@ -71,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
@@ -95,6 +101,7 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
@@ -112,6 +119,7 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
+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)
+6 -2
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:
@@ -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))
+17 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
@@ -46,6 +46,12 @@ def run():
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('-p', '--port', type='int', default=0,
help='Specify a custom port for ssh')
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()
@@ -64,7 +70,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:
@@ -72,8 +78,13 @@ def run():
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = ['ssh', node, cmd]
cmd = ex[node]['value']
if not isinstance(cmd, str) and not isinstance(cmd, bytes):
cmd = cmd.encode('utf-8')
if options.port:
cmdv = ['ssh', '-p', '{0}'.format(options.port), node, cmd]
else:
cmdv = ['ssh', node, cmd]
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
@@ -96,6 +107,7 @@ def run():
pernodeout[node] = []
pernodeout[node].append(data)
else:
data = client.stringify(data)
if options.nonodeprefix:
sys.stderr.write(data)
else:
@@ -113,6 +125,7 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
line = client.stringify(line)
if options.nonodeprefix:
sys.stdout.write(line)
else:
+38 -6
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -63,10 +63,16 @@ def _print_cfg(scfg):
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': []}
curr = e['databynode'][node]
storagebynode[node][curr['type'] + 's'].append(curr)
for node in storagebynode:
for disk in sorted(storagebynode[node]['disks'],
@@ -108,6 +114,7 @@ def createstorage(noderange, options, args):
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)
@@ -115,6 +122,8 @@ def createstorage(noderange, options, args):
'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))
@@ -130,6 +139,7 @@ def deletestorage(noderange, options, args):
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)):
@@ -147,13 +157,27 @@ def deletestorage(noderange, options, args):
print(repr(rsp))
# def setstorage(noderange, options, args):
# pass
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,
# 'set': setstorage,
'diskset': setdisk,
'delete': deletestorage,
'rm': deletestorage,
}
@@ -162,7 +186,7 @@ funmap = {
def main():
argparser = OptParser(
usage='Usage: %prog <noderange> [show|create|delete]',
usage='Usage: %prog <noderange> [show|create|delete|diskset]',
epilog='',
)
argparser.add_option('-r', '--raidlevel', type='int',
@@ -183,6 +207,14 @@ def main():
'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')
+9 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
@@ -64,7 +64,7 @@ def printerror(res, node=None):
def download_servicedata(noderange, media):
def download_servicedata(noderange, media, options):
global exitcode
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
@@ -73,6 +73,7 @@ def download_servicedata(noderange, media):
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', ()):
@@ -121,6 +122,10 @@ def main():
'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:
@@ -142,6 +147,6 @@ def main():
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media)
handler(noderange, media, options)
if __name__ == '__main__':
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('')
+69 -14
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,7 +15,10 @@
# 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
@@ -36,6 +39,19 @@ _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):
@@ -104,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):
@@ -204,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
@@ -220,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):
@@ -229,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:
@@ -322,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")
@@ -381,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):
@@ -391,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
@@ -405,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:
@@ -478,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:
@@ -652,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)
+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
+29 -28
View File
@@ -39,14 +39,15 @@ alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export
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=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
NUMARGS=$((COMP_CWORD+1))
if [ "${COMP_WORDS[COMP_CWORD]}" == '' ]; then
CMPARGS+=("")
fi
GENNED=""
@@ -73,7 +74,7 @@ function _confluent_generic_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
@@ -82,7 +83,7 @@ function _confluent_generic_completion()
}
_confluent_nodeidentify_completion()
{
COMP_CANDIDATES=("on,off -h")
COMP_CANDIDATES=("on,off,blink -h")
_confluent_generic_completion
}
@@ -109,7 +110,7 @@ _confluent_nodemedia_completion()
return
fi
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS -lt 3 ]; then
@@ -122,7 +123,7 @@ _confluent_nodefirmware_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
@@ -140,7 +141,7 @@ _confluent_nodeshell_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -c -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -c -- ${COMP_WORDS[COMP_CWORD]}))
return
fi
if [ $NUMARGS -gt 3 ]; then
@@ -158,7 +159,7 @@ _confluent_nodelicense_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "install list" -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -W "install list save delete" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
@@ -166,6 +167,11 @@ _confluent_nodelicense_completion()
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
@@ -176,7 +182,7 @@ _confluent_nodesupport_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[-1]}))
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[COMP_CWORD]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
@@ -203,41 +209,36 @@ _confluent_nn_completion()
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
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[-1]}"))
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
}
_confluent_nr_completion()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
fi
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
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[-1]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[COMP_CWORD]}"))
}
_confluent_ng_completion()
{
@@ -245,17 +246,17 @@ _confluent_ng_completion()
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${COMP_WORDS[COMP_CWORD]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
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[-1]}"))
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
@@ -270,7 +271,7 @@ 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_nr_completion nodelist
complete -F _confluent_nodeattrib_completion nodelist
complete -F _confluent_nodemedia_completion nodemedia
complete -F _confluent_nodepower_completion nodepower
complete -F _confluent_nr_completion noderemove
@@ -100,3 +100,5 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
## ATTRIBUTES
+5
View File
@@ -22,6 +22,11 @@ given as a node expression, as documented in the man page for nodeattribexpressi
If combined with `-x`, will show all differing values except those indicated
by `-x`
* `-e`, `--extra`:
Read settings that are generally not needed, but may be slow to retrieve.
Notably this includes the IMM category of Lenovo systems. The most popular
IMM settings are available through faster 'bmc' attributes.
* `-x`, `--exclude`:
Rather than listing only the specified configuration parameters, list all
attributes except for the specified ones
@@ -23,6 +23,9 @@ data may be filtered by various parameters, as denoted in the options below.
**nodediscover assign** performs manual discovery, assigning an entry to a node
identity or, using `-i`, using a csv file to assign nodes all at once. For
example, a spreadsheet of serial numbers to desired node names could be used.
Note that if you see that the host is unreachable, it may be due to the IP
address on the endpoint having changed since last detected. In such a case, it
may help to **clear** and try **assign** again.
**nodediscover rescan** requests the server to do an active sweep for new
devices. Generally every effort is made to passively detect devices as they
+5 -1
View File
@@ -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:
@@ -41,3 +41,5 @@ the attributes are set on the node versus a group to which a node belongs.
## SEE ALSO
nodeattrib(8), nodeattribexpressions(5)
## ATTRIBUTES
+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:
+8 -2
View File
@@ -3,9 +3,15 @@ nodelicense(8) -- Manage license keys on BMC
## SYNOPSIS
`nodelicense <noderange> [list|install <filename>]`
`nodelicense <noderange> [list|install <filename>|delete <license>|save <directory>]`
## DESCRIPTION
`nodelicense` shows and installs license keys on supported BMCs
`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.
+2 -2
View File
@@ -42,7 +42,7 @@ The attribute name may use a wildcard:
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`
@@ -54,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.
+21
View File
@@ -0,0 +1,21 @@
nodersync(8) -- Run rsync in parallel against a noderange
=========================================================================
## SYNOPSIS
`nodersync <file/directorylist> <noderange>:<destination>`
## DESCRIPTION
Supervises execution of rsync to push files or a directory tree to the specified
noderange. This will present progress as percentage for all nodes.
## OPTIONS
* `-m`:
Specify maximum number of nodes for noderange max.
* `-c`:
Specify how many rsync executions to do concurrently. If noderange
exceeds the count, then excess nodes will wait until one of the
active count completes.
+2 -2
View File
@@ -35,8 +35,8 @@ the noderange.
`$ 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
* 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`
+4 -1
View File
@@ -7,7 +7,7 @@ nodesupport(8) -- Utilities for interacting with vendor support
## DESCRIPTION
`nodesupport` provides capabilities associated with interactiong with support.
`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).
@@ -16,6 +16,9 @@ connects to the managed system, so it will download to the remote system if runn
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
+2
View File
@@ -1,4 +1,6 @@
#!/bin/sh
cd `dirname $0`
python3 addattribs.py || python2 addattribs.py
cd `dirname $0`/doc/man
mkdir -p ../../man/man1
mkdir -p ../../man/man5
@@ -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
+30 -3
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python
#!/usr/bin/python2
import argparse
import errno
import os
import pwd
import socket
import subprocess
import sys
@@ -22,7 +23,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:
@@ -40,6 +41,12 @@ def make_certificate():
'/etc/confluent/srvcert.pem -subj /CN='
'{0}'.format(socket.gethostname()).split(' ')):
raise Exception('Error generating certificate')
try:
uid = pwd.getpwnam('confluent').pw_uid
os.chown('/etc/confluent/privkey.pem', uid, -1)
os.chown('/etc/confluent/srvcert.pem', uid, -1)
except KeyError:
pass
print('Certificate generated successfully')
os.umask(umask)
@@ -61,7 +68,7 @@ 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}})
@@ -69,12 +76,28 @@ def join_collective(server, invitation):
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)
if 'error' in res:
print(res['error'])
return
if 'error' in res['collective']:
print(res['collective']['error'])
return
@@ -104,6 +127,8 @@ def main():
'collective member. Run collective invite -h for more information')
ic.add_argument('name', help='Name of server to invite to join 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 '
@@ -117,6 +142,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()
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
@@ -32,7 +32,7 @@ import confluent.main
import multiprocessing
if __name__ == '__main__':
multiprocessing.freeze_support()
confluent.main.run()
confluent.main.run(sys.argv)
#except:
# pass
#p.disable()
+8 -3
View File
@@ -1,11 +1,16 @@
from os.path import exists
import shutil
import socket
import subprocess
import tempfile
def get_openssl_conf_location():
# CentOS/RHAT
return '/etc/pki/tls/openssl.cnf'
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(' '))
@@ -47,4 +52,4 @@ def create_certificate():
)
if __name__ == '__main__':
create_certificate()
create_certificate()
+7 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
@@ -62,8 +62,11 @@ if args[0] == 'restore':
if pid is not None:
print("Confluent is running, must shut down to restore db")
sys.exit(1)
password = options.password
if options.interactivepassword:
password = getpass.getpass('Enter password to restore backup: ')
try:
cfm.restore_db_from_directory(dumpdir, options.password)
cfm.restore_db_from_directory(dumpdir, password)
except Exception as e:
print(str(e))
sys.exit(1)
@@ -82,11 +85,11 @@ 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)
cfm.dump_db_to_directory(dumpdir, options.password, options.redact,
cfm.dump_db_to_directory(dumpdir, password, options.redact,
options.skipkeys)
+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
+12 -4
View File
@@ -3,8 +3,12 @@ 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=""
fi
cd ..
mkdir -p /tmp/confluent # $DPKGNAME
@@ -24,15 +28,15 @@ install-scripts=/opt/confluent/bin
package=$DPKGNAME
EOF
python setup.py sdist > /dev/null 2>&1
py2dsc dist/*.tar.gz
$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/' debian/control
sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex, python-dateutil, python-pyopenssl, python-msgpack/' debian/control
else
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python-lxml, python-eficompressor, python-pycryptodome/' debian/control
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python3-lxml, python3-eficompressor, python3-pycryptodome, python3-websocket, python3-msgpack, python3-eventlet, python3-pyparsing, python3-pyte, python3-pyghmi, python3-paramiko/' debian/control
fi
if grep wheezy /etc/os-release; then
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
@@ -40,8 +44,12 @@ if [ "$OPKGNAME" = "confluent-server" ]; then
echo 'confluent_client confluent-client' >> debian/pydist-overrides
fi
fi
if ! grep wheezy /etc/os-release; then
sed -i 's/^Package: python3-/Package: /' debian/control
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
+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:
+3 -3
View File
@@ -64,12 +64,12 @@ class AsyncTermRelation(object):
# Need to keep an association of term object to async
# This allows the async handler to know the context of
# outgoing data to provide to calling code
def __init__(self, termid, async):
self.async = async
def __init__(self, termid, asynchdl):
self.asynchdl = asynchdl
self.termid = termid
def got_data(self, data):
self.async.add(self.termid, data)
self.asynchdl.add(self.termid, data)
class AsyncSession(object):
+131 -56
View File
@@ -23,11 +23,17 @@ 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 os
import pwd
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,35 +45,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
def _prune_passcache():
# This function makes sure we don't remember a passphrase in memory more
# than 10 seconds
while True:
curtime = time.time()
for passent in _passcache.iterkeys():
if passent[2] < curtime - 10:
del _passcache[passent]
eventlet.sleep(10)
_deniedbyrole = {
# This supersedes the above and is only consulted after the allowed has happened
'Operator': {
'update': [
'/node*/configuration/management_controller/users/*',
]
}
}
def _get_usertenant(name, tenant=False):
@@ -81,11 +111,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 +125,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 +147,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,21 +208,27 @@ 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)
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 hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
return authorize(user, element, 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
if 'cryptpass' in ucfg:
_passchecking[(user, tenant)] = True
# TODO(jbjohnso): WORKERPOOL
@@ -200,23 +257,41 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# delay as well
if crypt == crypted:
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant)
try:
pammy = PAM.pam()
pammy.start(_pamservice, user, credobj.pam_conv)
pammy.authenticate()
pammy.acct_mgmt()
del pammy
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant, skipuserobj=False)
except NameError:
pass
except PAM.error:
pass
return authorize(user, element, tenant, operation)
if pam:
pwe = None
try:
pwe = pwd.getpwnam(user)
except KeyError:
#pam won't work if the user doesn't exist, don't go further
eventlet.sleep(0.05) # stall even on test for existence of a username
return None
if os.getuid() != 0:
# confluent is running with reduced privilege, however, pam_unix refuses
# to let a non-0 user check anothers password.
# We will fork and the child will assume elevated privilege to
# get unix_chkpwd helper to enable checking /etc/shadow
pid = os.fork()
if not pid:
usergood = False
try:
# we change to the uid we are trying to authenticate as, because
# pam_unix uses unix_chkpwd which reque
os.setuid(pwe.pw_uid)
usergood = pam.authenticate(user, passphrase, service=_pamservice)
finally:
os._exit(0 if usergood else 1)
usergood = os.waitpid(pid, 0)[1] == 0
else:
# We are running as root, we don't need to fork in order to authenticate the
# user
usergood = pam.authenticate(user, passphrase, service=_pamservice)
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())
@@ -234,4 +309,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])
@@ -27,6 +27,7 @@ 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:
@@ -70,7 +71,10 @@ def connect_to_leader(cert=None, name=None, leader=None):
return False
with connecting:
with cfm._initlock:
tlvdata.recv(remote) # the banner
banner = tlvdata.recv(remote) # the banner
vers = banner.split()[2]
if vers != b'v2':
raise Exception('This instance only supports protocol 2, synchronize versions between collective members')
tlvdata.recv(remote) # authpassed... 0..
if name is None:
name = get_myname()
@@ -119,7 +123,7 @@ def connect_to_leader(cert=None, name=None, leader=None):
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:
@@ -148,11 +152,11 @@ 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, leader)
return True
def follow_leader(remote):
def follow_leader(remote, leader):
global currentleader
cleanexit = False
try:
@@ -164,8 +168,8 @@ def follow_leader(remote):
log.log({'info': 'Previous following cleanly closed',
'subsystem': 'collective'})
return
log.log({'info': 'Current leader has disappeared, restarting '
'collective membership', 'subsystem': 'collective'})
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
@@ -208,8 +212,7 @@ def handle_connection(connection, cert, request, local=False):
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 '
@@ -246,7 +249,23 @@ def handle_connection(connection, cert, request, local=False):
collinfo['quorum'] = True
except exc.DegradedCollective:
collinfo['quorum'] = False
tlvdata.send(connection, {'collective': collinfo})
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': {'status': 'Successfully deleted {0}'.format(todelete)}})
connection.close()
return
if 'invite' == operation:
try:
@@ -267,7 +286,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,
@@ -375,7 +395,7 @@ def handle_connection(connection, cert, request, local=False):
connection.close()
return
if (currentleader == connection.getpeername()[0] and
follower and follower.isAlive()):
follower and not follower.dead):
# if we are happily following this leader already, don't stir
# the pot
tlvdata.send(connection, {'status': 0})
@@ -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.
@@ -183,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',
},
@@ -267,8 +270,9 @@ node = {
},
'console.method': {
'description': ('Indicate the method used to access the console of '
'the managed node.'),
'validvalues': ('ssh', 'ipmi'),
'the managed node. If not specified, then console '
'is disabled'),
'validvalues': ('ssh', 'ipmi', 'tsmsol'),
},
# 'virtualization.host': {
# 'description': ('Hypervisor where this node does/should reside'),
@@ -298,6 +302,7 @@ node = {
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
'ipmi is used if not specified.'
},
'enclosure.bay': {
'description': 'The bay in the enclosure, if any',
+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
+347 -144
View File
@@ -46,7 +46,10 @@ import Cryptodome.Protocol.KDF as KDF
from Cryptodome.Cipher import AES
from Cryptodome.Hash import HMAC
from Cryptodome.Hash import SHA256
import anydbm as dbm
try:
import anydbm as dbm
except ModuleNotFoundError:
import dbm
import ast
import base64
import confluent.config.attributes as allattributes
@@ -57,7 +60,10 @@ import confluent.util
import confluent.netutil as netutil
import confluent.exceptions as exc
import copy
import cPickle
try:
import cPickle
except ModuleNotFoundError:
import pickle as cPickle
import errno
import eventlet
import eventlet.event as event
@@ -65,6 +71,7 @@ import eventlet.green.select as select
import eventlet.green.threading as gthread
import fnmatch
import json
import msgpack
import operator
import os
import random
@@ -74,6 +81,10 @@ import struct
import sys
import threading
import traceback
try:
unicode
except NameError:
unicode = str
_masterkey = None
@@ -99,6 +110,15 @@ _attraliases = {
'bmcpass': 'secret.hardwaremanagementpassword',
'switchpass': 'secret.hardwaremanagementpassword',
}
_validroles = ('Administrator', 'Operator', 'Monitor')
def attrib_supports_expression(attrib):
attrib = _attraliases.get(attrib, attrib)
if attrib.startswith('secret.') or attrib.startswith('crypted.'):
return False
return True
def _mkpath(pathname):
try:
@@ -144,10 +164,13 @@ def _parse_key(keydata, password=None):
if keydata.startswith('*unencrypted:'):
return base64.b64decode(keydata[13:])
elif password:
salt, iv, crypt, hmac = [base64.b64decode(x)
cryptbits = [base64.b64decode(x)
for x in keydata.split('!')]
salt, iv, crypt, hmac = cryptbits[:4]
privkey, integkey = _derive_keys(password, salt)
return decrypt_value([iv, crypt, hmac], privkey, integkey)
if len(cryptbits) > 4:
integkey = None
return decrypt_value(cryptbits[1:], privkey, integkey)
raise(exc.LockedCredentials(
"Passphrase protected secret requires password"))
@@ -156,7 +179,7 @@ def _format_key(key, password=None):
if password is not None:
salt = os.urandom(32)
privkey, integkey = _derive_keys(password, salt)
cval = crypt_value(key, key=privkey, integritykey=integkey)
cval = crypt_value(key, key=privkey) # , integritykey=integkey)
return {"passphraseprotected": (salt,) + cval}
else:
return {"unencryptedvalue": key}
@@ -169,6 +192,24 @@ def _do_notifier(cfg, watcher, callback):
logException()
def _rpc_master_del_usergroup(tenant, name):
ConfigManager(tenant).del_usergroup(name)
def _rpc_del_usergroup(tenant, name):
ConfigManager(tenant)._true_del_usergroup(name)
def _rpc_master_set_usergroup(tenant, name, attributemap):
ConfigManager(tenant).set_user(name, attributemap)
def _rpc_set_usergroup(tenant, name, attributemap):
ConfigManager(tenant)._true_set_user(name, attributemap)
def _rpc_master_set_user(tenant, name, attributemap):
ConfigManager(tenant).set_user(name, attributemap)
@@ -220,9 +261,19 @@ def _rpc_del_user(tenant, name):
def _rpc_master_create_user(tenant, *args):
ConfigManager(tenant).create_user(*args)
def _rpc_master_create_usergroup(tenant, *args):
ConfigManager(tenant).create_usergroup(*args)
def _rpc_create_user(tenant, *args):
ConfigManager(tenant)._true_create_user(*args)
def _rpc_create_usergroup(tenant, *args):
ConfigManager(tenant)._true_create_usergroup(*args)
def _rpc_master_del_groups(tenant, groups):
ConfigManager(tenant).del_groups(groups)
@@ -267,12 +318,12 @@ def check_quorum():
def exec_on_leader(function, *args):
if isinstance(cfgleader, bool):
raise exc.DegradedCollective()
xid = os.urandom(8)
xid = confluent.util.stringify(base64.b64encode(os.urandom(8)))
while xid in _pendingchangesets:
xid = os.urandom(8)
xid = confluent.util.stringify(base64.b64encode(os.urandom(8)))
_pendingchangesets[xid] = event.Event()
rpcpayload = cPickle.dumps({'function': function, 'args': args,
'xid': xid})
rpcpayload = msgpack.packb({'function': function, 'args': args,
'xid': xid}, use_bin_type=False)
rpclen = len(rpcpayload)
cfgleader.sendall(struct.pack('!Q', rpclen))
cfgleader.sendall(rpcpayload)
@@ -282,15 +333,24 @@ def exec_on_leader(function, *args):
def exec_on_followers(fnname, *args):
global _txcount
pushes = eventlet.GreenPool()
# Check health of collective prior to attempting
for _ in pushes.starmap(
_push_rpc, [(cfgstreams[s], b'') for s in cfgstreams]):
pass
if len(cfgstreams) < (len(_cfgstore['collective']) // 2):
# the leader counts in addition to registered streams
raise exc.DegradedCollective()
exec_on_followers_unconditional(fnname, *args)
def exec_on_followers_unconditional(fnname, *args):
global _txcount
pushes = eventlet.GreenPool()
_txcount += 1
payload = cPickle.dumps({'function': fnname, 'args': args,
'txcount': _txcount})
for res in pushes.starmap(
payload = msgpack.packb({'function': fnname, 'args': args,
'txcount': _txcount}, use_bin_type=False)
for _ in pushes.starmap(
_push_rpc, [(cfgstreams[s], payload) for s in cfgstreams]):
pass
@@ -327,42 +387,57 @@ def init_masterkey(password=None, autogen=True):
if cfgn:
_masterintegritykey = _get_protected_key(cfgn, password,
'master_integrity_key')
elif autogen:
_masterintegritykey = os.urandom(64)
set_global('master_integrity_key', _format_key(
_masterintegritykey,
password=password))
#elif autogen:
# _masterintegritykey = os.urandom(64)
# set_global('master_integrity_key', _format_key(
# _masterintegritykey,
# password=password))
def _push_rpc(stream, payload):
with _rpclock:
stream.sendall(struct.pack('!Q', len(payload)))
if len(payload):
stream.sendall(payload)
try:
stream.sendall(struct.pack('!Q', len(payload)))
if len(payload):
stream.sendall(payload)
return True
except Exception:
logException()
del cfgstreams[stream]
stream.close()
def decrypt_value(cryptvalue,
key=None,
integritykey=None):
iv, cipherdata, hmac = cryptvalue
# for future reference, if cryptvalue len == 3, then cbc+hmac, 4 includes version
iv, cipherdata, hmac = cryptvalue[:3]
if key is None and integritykey is None:
if _masterkey is None or _masterintegritykey is None:
if _masterkey is None:
init_masterkey(autogen=False)
key = _masterkey
integritykey = _masterintegritykey
check_hmac = HMAC.new(integritykey, cipherdata, SHA256).digest()
if hmac != check_hmac:
raise Exception("bad HMAC value on crypted value")
decrypter = AES.new(key, AES.MODE_CBC, iv)
value = decrypter.decrypt(cipherdata)
padsize = ord(value[-1])
pad = value[-padsize:]
# Note that I cannot grasp what could be done with a subliminal
# channel in padding in this case, but check the padding anyway
for padbyte in pad:
if ord(padbyte) != padsize:
raise Exception("bad padding in encrypted value")
return value[0:-padsize]
if len(cryptvalue) == 3:
check_hmac = HMAC.new(integritykey, cipherdata, SHA256).digest()
if hmac != check_hmac:
check_hmac = HMAC.new(integritykey, cipherdata + iv, SHA256).digest()
if hmac != check_hmac:
raise Exception("bad HMAC value on crypted value")
decrypter = AES.new(key, AES.MODE_CBC, iv)
value = decrypter.decrypt(cipherdata)
padsize = bytearray(value)[-1]
pad = value[-padsize:]
# Note that I cannot grasp what could be done with a subliminal
# channel in padding in this case, but check the padding anyway
for padbyte in bytearray(pad):
if padbyte != padsize:
raise Exception("bad padding in encrypted value")
return value[0:-padsize]
else:
decrypter = AES.new(key, AES.MODE_GCM, nonce=iv)
value = decrypter.decrypt(cipherdata)
decrypter.verify(hmac)
return value
def fixup_attribute(attrname, attrval):
@@ -414,22 +489,17 @@ def crypt_value(value,
# encrypt given value
# PKCS7 is the padding scheme to employ, if no padded needed, pad with 16
# check HMAC prior to attempting decrypt
if key is None or integritykey is None:
if _masterkey is None or _masterintegritykey is None:
hmac = None
if key is None:
if _masterkey is None:
init_masterkey()
key = _masterkey
integritykey = _masterintegritykey
iv = os.urandom(16)
crypter = AES.new(key, AES.MODE_CBC, iv)
neededpad = 16 - (len(value) % 16)
pad = chr(neededpad) * neededpad
value += pad
try:
cryptval = crypter.encrypt(value)
except TypeError:
cryptval = crypter.encrypt(value.encode('utf-8'))
hmac = HMAC.new(integritykey, cryptval, SHA256).digest()
return iv, cryptval, hmac
iv = os.urandom(12)
crypter = AES.new(key, AES.MODE_GCM, nonce=iv)
if not isinstance(value, bytes):
value = value.encode('utf-8')
cryptval, hmac = crypter.encrypt_and_digest(value)
return iv, cryptval, hmac, b'\x02'
def _load_dict_from_dbm(dpath, tdb):
@@ -437,16 +507,19 @@ def _load_dict_from_dbm(dpath, tdb):
dbe = dbm.open(tdb, 'r')
currdict = _cfgstore
for elem in dpath:
elem = confluent.util.stringify(elem)
if elem not in currdict:
currdict[elem] = {}
currdict = currdict[elem]
try:
for tk in dbe:
currdict[tk] = cPickle.loads(dbe[tk])
for tk in dbe.keys():
tks = confluent.util.stringify(tk)
currdict[tks] = cPickle.loads(dbe[tk])
except AttributeError:
tk = dbe.firstkey()
while tk != None:
currdict[tk] = cPickle.loads(dbe[tk])
tks = confluent.util.stringify(tk)
currdict[tks] = cPickle.loads(dbe[tk])
tk = dbe.nextkey(tk)
except dbm.error:
return
@@ -485,13 +558,7 @@ def set_global(globalname, value, sync=True):
"""
if _cfgstore is None:
init(not sync)
try:
globalname = globalname.encode('utf-8')
except AttributeError:
# We have to remove the unicode-ness of the string,
# but if it is already bytes in python 3, then we will
# get an attributeerror, so pass
pass
globalname = confluent.util.stringify(globalname)
with _dirtylock:
if 'dirtyglobals' not in _cfgstore:
_cfgstore['dirtyglobals'] = set()
@@ -523,11 +590,18 @@ def relay_slaved_requests(name, listener):
lh = StreamHandler(listener)
_hasquorum = len(cfgstreams) >= (
len(_cfgstore['collective']) // 2)
payload = cPickle.dumps({'quorum': _hasquorum})
for _ in pushes.starmap(
_push_rpc,
[(cfgstreams[s], payload) for s in cfgstreams]):
pass
_newquorum = None
while _hasquorum != _newquorum:
if _newquorum is not None:
_hasquorum = _newquorum
payload = msgpack.packb({'quorum': _hasquorum}, use_bin_type=False)
for _ in pushes.starmap(
_push_rpc,
[(cfgstreams[s], payload) for s in cfgstreams]):
pass
_newquorum = len(cfgstreams) >= (
len(_cfgstore['collective']) // 2)
_hasquorum = _newquorum
if _hasquorum and _pending_collective_updates:
apply_pending_collective_updates()
msg = lh.get_next_msg()
@@ -536,21 +610,27 @@ def relay_slaved_requests(name, listener):
raise Exception("Unexpected loss of node in followers: " + name)
sz = struct.unpack('!Q', msg)[0]
if sz != 0:
rpc = ''
rpc = b''
while len(rpc) < sz:
nrpc = listener.recv(sz - len(rpc))
if not nrpc:
raise Exception('Truncated client error')
rpc += nrpc
rpc = cPickle.loads(rpc)
rpc = msgpack.unpackb(rpc, raw=False)
exc = None
if not (rpc['function'].startswith('_rpc_') or rpc['function'].endswith('_collective_member')):
raise Exception('Unsupported function {0} called'.format(rpc['function']))
try:
globals()[rpc['function']](*rpc['args'])
except ValueError as ve:
exc = ['ValueError', str(ve)]
except Exception as e:
exc = e
exc = ['Exception', str(e)]
if 'xid' in rpc:
_push_rpc(listener, cPickle.dumps({'xid': rpc['xid'],
'exc': exc}))
res = _push_rpc(listener, msgpack.packb({'xid': rpc['xid'],
'exc': exc}, use_bin_type=False))
if not res:
break
try:
msg = lh.get_next_msg()
except Exception:
@@ -567,7 +647,7 @@ def relay_slaved_requests(name, listener):
if cfgstreams:
_hasquorum = len(cfgstreams) >= (
len(_cfgstore['collective']) // 2)
payload = cPickle.dumps({'quorum': _hasquorum})
payload = msgpack.packb({'quorum': _hasquorum}, use_bin_type=False)
for _ in pushes.starmap(
_push_rpc,
[(cfgstreams[s], payload) for s in cfgstreams]):
@@ -595,7 +675,9 @@ class StreamHandler(object):
if confluent.util.monotonic_time() > self.expiry:
return None
if confluent.util.monotonic_time() > self.keepalive:
_push_rpc(self.sock, b'') # nulls are a keepalive
res = _push_rpc(self.sock, b'') # nulls are a keepalive
if not res:
return None
self.keepalive = confluent.util.monotonic_time() + 20
self.expiry = confluent.util.monotonic_time() + 60
msg = self.sock.recv(8)
@@ -686,28 +768,37 @@ def follow_channel(channel):
while msg:
sz = struct.unpack('!Q', msg)[0]
if sz != 0:
rpc = ''
rpc = b''
while len(rpc) < sz:
nrpc = channel.recv(sz - len(rpc))
if not nrpc:
raise Exception('Truncated message error')
rpc += nrpc
rpc = cPickle.loads(rpc)
rpc = msgpack.unpackb(rpc, raw=False)
if 'txcount' in rpc:
_txcount = rpc['txcount']
if 'function' in rpc:
if not (rpc['function'].startswith('_true') or rpc['function'].startswith('_rpc')):
raise Exception("Received unsupported function call: {0}".format(rpc['function']))
try:
globals()[rpc['function']](*rpc['args'])
except Exception as e:
print(repr(e))
if 'xid' in rpc and rpc['xid']:
if rpc.get('exc', None):
_pendingchangesets[rpc['xid']].send_exception(rpc['exc'])
exctype, excstr = rpc['exc']
if exctype == 'ValueError':
exc = ValueError(excstr)
else:
exc = Exception(excstr)
_pendingchangesets[rpc['xid']].send_exception(exc)
else:
_pendingchangesets[rpc['xid']].send()
if 'quorum' in rpc:
_hasquorum = rpc['quorum']
_push_rpc(channel, b'') # use null as ACK
res = _push_rpc(channel, b'') # use null as ACK
if not res:
break
msg = lh.get_next_msg()
finally:
# mark the connection as broken
@@ -724,6 +815,34 @@ def add_collective_member(name, address, fingerprint):
exec_on_followers('_true_add_collective_member', name, address, fingerprint)
_true_add_collective_member(name, address, fingerprint)
def del_collective_member(name):
if cfgleader and not isinstance(cfgleader, bool):
return exec_on_leader('del_collective_member', name)
if cfgstreams:
exec_on_followers_unconditional('_true_del_collective_member', name)
_true_del_collective_member(name)
def _true_del_collective_member(name, sync=True):
global cfgleader
name = confluent.util.stringify(name)
if _cfgstore is None:
return
if 'collective' not in _cfgstore:
return
if name not in _cfgstore['collective']:
return
del _cfgstore['collective'][name]
with _dirtylock:
if 'collectivedirty' not in _cfgstore:
_cfgstore['collectivedirty'] = set([])
_cfgstore['collectivedirty'].add(name)
if len(_cfgstore['collective']) < 2:
del _cfgstore['collective']
cfgleader = None
if sync:
ConfigManager._bg_sync_to_file()
_pending_collective_updates = {}
@@ -748,10 +867,7 @@ def apply_pending_collective_updates():
def _true_add_collective_member(name, address, fingerprint, sync=True):
try:
name = name.encode('utf-8')
except AttributeError:
pass
name = confluent.util.stringify(name)
if _cfgstore is None:
init(not sync) # use not sync to avoid read from disk
if 'collective' not in _cfgstore:
@@ -786,8 +902,7 @@ def get_collective_member_by_address(address):
def _mark_dirtykey(category, key, tenant=None):
if type(key) in (str, unicode):
key = key.encode('utf-8')
key = confluent.util.stringify(key)
with _dirtylock:
if 'dirtykeys' not in _cfgstore:
_cfgstore['dirtykeys'] = {}
@@ -1053,6 +1168,8 @@ class ConfigManager(object):
attribute, match = expression.split('=')
else:
raise Exception('Invalid Expression')
if attribute.startswith('secret.'):
raise Exception('Filter by secret attributes is not supported')
for node in nodes:
try:
currvals = [self._cfgstore['nodes'][node][attribute]['value']]
@@ -1108,9 +1225,9 @@ class ConfigManager(object):
Returns an identifier that can be used to unsubscribe from these
notifications using remove_watcher
"""
notifierid = random.randint(0, sys.maxint)
notifierid = random.randint(0, sys.maxsize)
while notifierid in self._notifierids:
notifierid = random.randint(0, sys.maxint)
notifierid = random.randint(0, sys.maxsize)
self._notifierids[notifierid] = {'attriblist': []}
if self.tenant not in self._attribwatchers:
self._attribwatchers[self.tenant] = {}
@@ -1149,9 +1266,9 @@ class ConfigManager(object):
# use in case of cancellation.
# I anticipate no more than a handful of watchers of this sort, so
# this loop should not have to iterate too many times
notifierid = random.randint(0, sys.maxint)
notifierid = random.randint(0, sys.maxsize)
while notifierid in self._notifierids:
notifierid = random.randint(0, sys.maxint)
notifierid = random.randint(0, sys.maxsize)
# going to track that this is a nodecollection type watcher,
# but there is no additional data associated.
self._notifierids[notifierid] = set(['nodecollection'])
@@ -1182,6 +1299,12 @@ class ConfigManager(object):
except KeyError:
return []
def list_usergroups(self):
try:
return list(self._cfgstore['usergroups'])
except KeyError:
return []
def get_user(self, name):
"""Get user information from DB
@@ -1219,19 +1342,67 @@ class ConfigManager(object):
:param groupname: the name of teh group to modify
:param attributemap: The mapping of keys to values to set
"""
if cfgleader:
return exec_on_leader('_rpc_master_set_usergroup', self.tenant,
groupname, attributemap)
if cfgstreams:
exec_on_followers('_rpc_set_usergroup', self.tenant, groupname,
attributemap)
self._true_set_usergroup(groupname, attributemap)
def _true_set_usergroup(self, groupname, attributemap):
for attribute in attributemap:
self._cfgstore['usergroups'][attribute] = attributemap[attribute]
if attribute == 'role':
therole = None
for candrole in _validroles:
if candrole.lower().startswith(attributemap[attribute].lower()):
therole = candrole
if therole not in _validroles:
raise ValueError(
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
attributemap[attribute] = therole
self._cfgstore['usergroups'][groupname][attribute] = attributemap[attribute]
_mark_dirtykey('usergroups', groupname, self.tenant)
self._bg_sync_to_file()
def create_usergroup(self, groupname, role="Administrator"):
"""Create a new user
:param groupname: The name of the user group
:param role: The role the user should be considered. Can be
"Administrator" or "Technician", defaults to
"Administrator"
"""
if cfgleader:
return exec_on_leader('_rpc_master_create_usergroup', self.tenant,
groupname, role)
if cfgstreams:
exec_on_followers('_rpc_create_usergroup', self.tenant, groupname,
role)
self._true_create_usergroup(groupname, role)
def _true_create_usergroup(self, groupname, role="Administrator"):
if 'usergroups' not in self._cfgstore:
self._cfgstore['usergroups'] = {}
groupname = groupname.encode('utf-8')
groupname = confluent.util.stringify(groupname)
if groupname in self._cfgstore['usergroups']:
raise Exception("Duplicate groupname requested")
self._cfgstore['usergroups'][groupname] = {'role': role}
_mark_dirtykey('usergroups', groupname, self.tenant)
self._bg_sync_to_file()
def del_usergroup(self, name):
if cfgleader:
return exec_on_leader('_rpc_master_del_usergroup', self.tenant, name)
if cfgstreams:
exec_on_followers('_rpc_del_usergroup', self.tenant, name)
self._true_del_usergroup(name)
def _true_del_usergroup(self, name):
if name in self._cfgstore['usergroups']:
del self._cfgstore['usergroups'][name]
_mark_dirtykey('usergroups', name, self.tenant)
self._bg_sync_to_file()
def set_user(self, name, attributemap):
"""Set user attribute(s)
@@ -1243,12 +1414,21 @@ class ConfigManager(object):
return exec_on_leader('_rpc_master_set_user', self.tenant, name,
attributemap)
if cfgstreams:
exec_on_followers('_rpc_set_user', self.tenant, name)
exec_on_followers('_rpc_set_user', self.tenant, name, attributemap)
self._true_set_user(name, attributemap)
def _true_set_user(self, name, attributemap):
user = self._cfgstore['users'][name]
for attribute in attributemap:
if attribute == 'role':
therole = None
for candrole in _validroles:
if candrole.lower().startswith(attributemap[attribute].lower()):
therole = candrole
if therole not in _validroles:
raise ValueError(
'Unrecognized role "{0}" (valid roles: {1})'.format(attributemap[attribute], ','.join(_validroles)))
attributemap[attribute] = therole
if attribute == 'password':
salt = os.urandom(8)
#TODO: WORKERPOOL, offload password set to a worker
@@ -1306,7 +1486,7 @@ class ConfigManager(object):
raise Exception("Duplicate id requested")
if 'users' not in self._cfgstore:
self._cfgstore['users'] = {}
name = name.encode('utf-8')
name = confluent.util.stringify(name)
if name in self._cfgstore['users']:
raise Exception("Duplicate username requested")
self._cfgstore['users'][name] = {'id': uid}
@@ -1314,9 +1494,10 @@ class ConfigManager(object):
self._cfgstore['users'][name]['displayname'] = displayname
_cfgstore['main']['idmap'][uid] = {
'tenant': self.tenant,
'username': name
'username': name,
'role': role,
}
if attributemap is not None:
if attributemap:
self._true_set_user(name, attributemap)
_mark_dirtykey('users', name, self.tenant)
_mark_dirtykey('idmap', uid)
@@ -1479,6 +1660,7 @@ class ConfigManager(object):
if group in self._cfgstore['nodes'][node]['groups']:
self._cfgstore['nodes'][node]['groups'].remove(group)
self._node_removed_from_group(node, group, changeset)
_mark_dirtykey('nodes', node, self.tenant)
for node in nodes:
if node not in self._cfgstore['nodes']:
self._cfgstore['nodes'][node] = {'groups': [group]}
@@ -1525,7 +1707,7 @@ class ConfigManager(object):
del attribmap[group][attr]
if 'noderange' in attribmap[group]:
if len(attribmap[group]) > 1:
raise ValueErorr('noderange attribute must be set by itself')
raise ValueError('noderange attribute must be set by itself')
for attr in attribmap[group]:
if attr in _attraliases:
newattr = _attraliases[attr]
@@ -1564,7 +1746,7 @@ class ConfigManager(object):
"{0} node does not exist to add to {1}".format(
node, group))
for group in attribmap:
group = group.encode('utf-8')
group = confluent.util.stringify(group)
if group not in self._cfgstore['nodegroups']:
self._cfgstore['nodegroups'][group] = {'nodes': set()}
cfgobj = self._cfgstore['nodegroups'][group]
@@ -1622,8 +1804,8 @@ class ConfigManager(object):
attributes = realattributes
if type(groups) in (str, unicode):
groups = (groups,)
for group in groups:
group = group.encode('utf-8')
for group in groups:
group = confluent.util.stringify(group)
try:
groupentry = self._cfgstore['nodegroups'][group]
except KeyError:
@@ -1714,11 +1896,14 @@ class ConfigManager(object):
'nodeattrs': {node: [attrname]},
'callback': attribwatcher[watchkey][notifierid]
}
for watcher in notifdata.itervalues():
for watcher in notifdata:
watcher = notifdata[watcher]
callback = watcher['callback']
eventlet.spawn_n(_do_notifier, self, watcher, callback)
def del_nodes(self, nodes):
if isinstance(nodes, set):
nodes = list(nodes) # msgpack can't handle set
if cfgleader: # slaved to a collective
return exec_on_leader('_rpc_master_del_nodes', self.tenant,
nodes)
@@ -1728,14 +1913,15 @@ class ConfigManager(object):
def _true_del_nodes(self, nodes):
if self.tenant in self._nodecollwatchers:
for watcher in self._nodecollwatchers[self.tenant].itervalues():
for watcher in self._nodecollwatchers[self.tenant]:
watcher = self._nodecollwatchers[self.tenant][watcher]
watcher(added=(), deleting=nodes, renamed=(), configmanager=self)
changeset = {}
for node in nodes:
# set a reserved attribute for the sake of the change notification
# framework to trigger on
changeset[node] = {'_nodedeleted': 1}
node = node.encode('utf-8')
node = confluent.util.stringify(node)
if node in self._cfgstore['nodes']:
self._sync_groups_to_node(node=node, groups=[],
changeset=changeset)
@@ -1783,7 +1969,7 @@ class ConfigManager(object):
realattributes.append(attrname)
attributes = realattributes
for node in nodes:
node = node.encode('utf-8')
node = confluent.util.stringify(node)
try:
nodek = self._cfgstore['nodes'][node]
except KeyError:
@@ -1854,7 +2040,8 @@ class ConfigManager(object):
self._recalculate_expressions(cfgobj, formatter=exprmgr, node=renamemap[name], changeset=changeset)
if self.tenant in self._nodecollwatchers:
nodecollwatchers = self._nodecollwatchers[self.tenant]
for watcher in nodecollwatchers.itervalues():
for watcher in nodecollwatchers:
watcher = nodecollwatchers[watcher]
eventlet.spawn_n(_do_add_watcher, watcher, (), self, renamemap)
self._bg_sync_to_file()
@@ -1911,7 +2098,7 @@ class ConfigManager(object):
# first do a sanity check of the input upfront
# this mitigates risk of arguments being partially applied
for node in attribmap:
node = node.encode('utf-8')
node = confluent.util.stringify(node)
if node == '':
raise ValueError('"{0}" is not a valid node name'.format(node))
if autocreate:
@@ -1966,8 +2153,8 @@ class ConfigManager(object):
attrname, node)
raise ValueError(errstr)
attribmap[node][attrname] = attrval
for node in attribmap:
node = node.encode('utf-8')
for node in attribmap:
node = confluent.util.stringify(node)
exprmgr = None
if node not in self._cfgstore['nodes']:
newnodes.append(node)
@@ -2010,7 +2197,8 @@ class ConfigManager(object):
if newnodes:
if self.tenant in self._nodecollwatchers:
nodecollwatchers = self._nodecollwatchers[self.tenant]
for watcher in nodecollwatchers.itervalues():
for watcher in nodecollwatchers:
watcher = nodecollwatchers[watcher]
eventlet.spawn_n(_do_add_watcher, watcher, newnodes, self)
self._bg_sync_to_file()
#TODO: wait for synchronization to suceed/fail??)
@@ -2077,6 +2265,9 @@ class ConfigManager(object):
self.set_node_attributes(tmpconfig[confarea], True)
elif confarea == 'nodegroups':
self.set_group_attributes(tmpconfig[confarea], True)
elif confarea == 'usergroups':
for usergroup in tmpconfig[confarea]:
self.create_usergroup(usergroup)
elif confarea == 'users':
for user in tmpconfig[confarea]:
uid = tmpconfig[confarea].get('id', None)
@@ -2125,7 +2316,7 @@ class ConfigManager(object):
target = dumpdata[confarea][element][attribute]['cryptvalue']
cryptval = []
for value in target:
cryptval.append(base64.b64encode(value))
cryptval.append(confluent.util.stringify(base64.b64encode(value)))
if attribute == 'cryptpass':
dumpdata[confarea][element][attribute] = '!'.join(cryptval)
else:
@@ -2145,7 +2336,7 @@ class ConfigManager(object):
_cfgstore = {}
rootpath = cls._cfgdir
try:
with open(os.path.join(rootpath, 'transactioncount'), 'r') as f:
with open(os.path.join(rootpath, 'transactioncount'), 'rb') as f:
txbytes = f.read()
if len(txbytes) == 8:
_txcount = struct.unpack('!Q', txbytes)[0]
@@ -2203,7 +2394,7 @@ class ConfigManager(object):
if statelessmode:
return
_mkpath(cls._cfgdir)
with open(os.path.join(cls._cfgdir, 'transactioncount'), 'w') as f:
with open(os.path.join(cls._cfgdir, 'transactioncount'), 'wb') as f:
f.write(struct.pack('!Q', _txcount))
if (fullsync or 'dirtyglobals' in _cfgstore and
'globals' in _cfgstore):
@@ -2218,31 +2409,37 @@ class ConfigManager(object):
for globalkey in dirtyglobals:
if globalkey in _cfgstore['globals']:
globalf[globalkey] = \
cPickle.dumps(_cfgstore['globals'][globalkey])
cPickle.dumps(_cfgstore['globals'][globalkey], protocol=cPickle.HIGHEST_PROTOCOL)
else:
if globalkey in globalf:
del globalf[globalkey]
finally:
globalf.close()
if fullsync or 'collectivedirty' in _cfgstore:
collectivef = dbm.open(os.path.join(cls._cfgdir, "collective"),
'c', 384)
try:
if fullsync:
colls = _cfgstore['collective']
else:
with _dirtylock:
colls = copy.deepcopy(_cfgstore['collectivedirty'])
del _cfgstore['collectivedirty']
for coll in colls:
if coll in _cfgstore['collective']:
collectivef[coll] = cPickle.dumps(
_cfgstore['collective'][coll])
if len(_cfgstore.get('collective', ())) > 1:
collectivef = dbm.open(os.path.join(cls._cfgdir, "collective"),
'c', 384)
try:
if fullsync:
colls = _cfgstore['collective']
else:
if coll in collectivef:
del globalf[coll]
finally:
collectivef.close()
with _dirtylock:
colls = copy.deepcopy(_cfgstore['collectivedirty'])
del _cfgstore['collectivedirty']
for coll in colls:
if coll in _cfgstore['collective']:
collectivef[coll] = cPickle.dumps(
_cfgstore['collective'][coll], protocol=cPickle.HIGHEST_PROTOCOL)
else:
if coll in collectivef:
del collectivef[coll]
finally:
collectivef.close()
else:
try:
os.remove(os.path.join(cls._cfgdir, "collective"))
except OSError:
pass
if fullsync:
pathname = cls._cfgdir
currdict = _cfgstore['main']
@@ -2251,7 +2448,7 @@ class ConfigManager(object):
dbf = dbm.open(os.path.join(pathname, category), 'c', 384) # 0600
try:
for ck in currdict[category]:
dbf[ck] = cPickle.dumps(currdict[category][ck])
dbf[ck] = cPickle.dumps(currdict[category][ck], protocol=cPickle.HIGHEST_PROTOCOL)
finally:
dbf.close()
elif 'dirtykeys' in _cfgstore:
@@ -2275,7 +2472,7 @@ class ConfigManager(object):
if ck in dbf:
del dbf[ck]
else:
dbf[ck] = cPickle.dumps(currdict[category][ck])
dbf[ck] = cPickle.dumps(currdict[category][ck], protocol=cPickle.HIGHEST_PROTOCOL)
finally:
dbf.close()
willrun = False
@@ -2314,7 +2511,9 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
else:
keydata = json.loads(jsond)
cryptkey = _parse_key(keydata['cryptkey'], password)
integritykey = _parse_key(keydata['integritykey'], password)
integritykey = None
if 'integritykey' in keydata:
integritykey = _parse_key(keydata['integritykey'], password)
conf.init_config()
cfg = conf.get_config()
if cfg.has_option('security', 'externalcfgkey'):
@@ -2323,8 +2522,9 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
newpassword = keyfile.read()
set_global('master_privacy_key', _format_key(cryptkey,
password=newpassword), sync)
set_global('master_integrity_key', _format_key(integritykey,
password=newpassword), sync)
if integritykey:
set_global('master_integrity_key', _format_key(integritykey,
password=newpassword), sync)
_masterkey = cryptkey
_masterintegritykey = integritykey
if sync:
@@ -2332,23 +2532,26 @@ def _restore_keys(jsond, password, newpassword=None, sync=True):
def _dump_keys(password, dojson=True):
if _masterkey is None or _masterintegritykey is None:
if _masterkey is None:
init_masterkey()
cryptkey = _format_key(_masterkey, password=password)
if 'passphraseprotected' in cryptkey:
cryptkey = '!'.join(map(base64.b64encode,
cryptkey['passphraseprotected']))
cryptkey = '!'.join(
[confluent.util.stringify(base64.b64encode(x))
for x in cryptkey['passphraseprotected']])
else:
cryptkey = '*unencrypted:{0}'.format(base64.b64encode(
cryptkey['unencryptedvalue']))
integritykey = _format_key(_masterintegritykey, password=password)
if 'passphraseprotected' in integritykey:
integritykey = '!'.join(map(base64.b64encode,
integritykey['passphraseprotected']))
else:
integritykey = '*unencrypted:{0}'.format(base64.b64encode(
integritykey['unencryptedvalue']))
keydata = {'cryptkey': cryptkey, 'integritykey': integritykey}
cryptkey = '*unencrypted:{0}'.format(confluent.util.stringify(base64.b64encode(
cryptkey['unencryptedvalue'])))
keydata = {'cryptkey': cryptkey}
if _masterintegritykey is not None:
integritykey = _format_key(_masterintegritykey, password=password)
if 'passphraseprotected' in integritykey:
integritykey = '!'.join([confluent.util.stringify(base64.b64encode(x)) for x in
integritykey['passphraseprotected']])
else:
integritykey = '*unencrypted:{0}'.format(confluent.util.stringify(base64.b64encode(
integritykey['unencryptedvalue'])))
keydata['integritykey'] = integritykey
if dojson:
return json.dumps(keydata, sort_keys=True, indent=4, separators=(',', ': '))
return keydata
+41 -28
View File
@@ -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,12 +106,12 @@ 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])
csi.append(30 + pytecolors2ansi.get(char.fg, 9))
lfg = char.fg
if char.bg != lbg:
csi.append(40 + pytecolors2ansi[char.bg])
csi.append(40 + pytecolors2ansi.get(char.bg, 9))
lbg = char.bg
if char.bold != lb:
lb = char.bold
@@ -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
@@ -185,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
@@ -210,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
@@ -238,7 +243,7 @@ class ConsoleHandler(object):
def check_collective(self, attrvalue):
myc = attrvalue.get(self.node, {}).get('collective.manager', {}).get(
'value', None)
if configmodule.list_collective() and not myc:
if list(configmodule.list_collective()) and not myc:
self._is_local = False
self._detach()
self._disconnect()
@@ -357,13 +362,17 @@ 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:
except Exception as e:
strerror = str(e)
if _tracelog:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
@@ -376,13 +385,9 @@ class ConsoleHandler(object):
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
self.feedbuffer(
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
'not configured,\r\nset it to a valid value for console '
'function]')
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
self._send_rcpts(
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
'not configured,\r\nset it to a valid value for console '
'function]')
'\x1bc\x1b[2J\x1b[1;1H[{0}]'.format(strerror))
self.clearerror = True
return
if self.clearerror:
@@ -448,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,
@@ -525,17 +531,19 @@ 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
@@ -588,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,
+145 -25
View File
@@ -60,18 +60,19 @@ import eventlet.greenpool as greenpool
import eventlet.green.ssl as ssl
import eventlet.queue as queue
import itertools
import msgpack
import os
try:
import cPickle as pickle
except ImportError:
import pickle
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')
try:
unicode
except NameError:
unicode = str
def seek_element(currplace, currkey):
try:
@@ -86,7 +87,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")
@@ -106,6 +110,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(
@@ -124,7 +130,7 @@ def load_plugins():
rootcollections = ['discovery/', 'events/', 'networking/',
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
'noderange/', 'nodes/', 'nodegroups/', 'usergroups/' , 'users/', 'version']
class PluginRoute(object):
@@ -163,6 +169,10 @@ def _init_core():
'default': 'ipmi',
}),
},
'clear': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'users': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -171,6 +181,10 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'save_licenses': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -201,6 +215,20 @@ def _init_core():
'default': 'ipmi',
}),
},
'extended': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'extra': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'advanced': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'storage': {
'all': PluginRoute({
@@ -380,6 +408,7 @@ def _init_core():
nodegroupresources = {
'attributes': {
'check': PluginRoute({'handler': 'attributes'}),
'rename': PluginRoute({'handler': 'attributes'}),
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
@@ -391,22 +420,46 @@ def create_user(inputdata, configmanager):
try:
username = inputdata['name']
del inputdata['name']
role = inputdata['role']
del inputdata['role']
except (KeyError, ValueError):
raise exc.InvalidArgumentException()
configmanager.create_user(username, attributemap=inputdata)
raise exc.InvalidArgumentException('Missing user name or role')
configmanager.create_user(username, role, attributemap=inputdata)
def create_usergroup(inputdata, configmanager):
try:
groupname = inputdata['name']
role = inputdata['role']
del inputdata['name']
del inputdata['role']
except (KeyError, ValueError):
raise exc.InvalidArgumentException("Missing user name or role")
configmanager.create_usergroup(groupname, role)
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:
@@ -419,6 +472,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):
@@ -451,6 +508,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
@@ -635,13 +696,22 @@ def handle_dispatch(connection, cert, dispatch, peername):
cfm.get_collective_member(peername)['fingerprint'], cert):
connection.close()
return
dispatch = pickle.loads(dispatch)
if dispatch[0:2] != b'\x01\x03': # magic value to indicate msgpack
# We only support msgpack now
# The magic should preclude any pickle, as the first byte can never be
# under 0x20 or so.
connection.close()
return
dispatch = msgpack.unpackb(dispatch[2:], raw=False)
configmanager = cfm.ConfigManager(dispatch['tenant'])
nodes = dispatch['nodes']
inputdata = dispatch['inputdata']
operation = dispatch['operation']
pathcomponents = dispatch['path']
routespec = nested_lookup(noderesources, pathcomponents)
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes, dispatch['isnoderange'],
configmanager)
plugroute = routespec.routeinfo
plugpath = None
nodesbyhandler = {}
@@ -678,7 +748,19 @@ def handle_dispatch(connection, cert, dispatch, peername):
def _forward_rsp(connection, res):
r = pickle.dumps(res)
try:
r = res.serialize()
except AttributeError:
if isinstance(res, Exception):
r = msgpack.packb(['Exception', str(res)], use_bin_type=False)
else:
r = msgpack.packb(
['Exception', 'Unable to serialize response ' + repr(res)],
use_bin_type=False)
except Exception as e:
r = msgpack.packb(
['Exception', 'Unable to serialize response ' + repr(res) + ' due to ' + str(e)],
use_bin_type=False)
rlen = len(r)
if not rlen:
return
@@ -769,7 +851,7 @@ def handle_node_request(configmanager, inputdata, operation,
del pathcomponents[0:2]
passvalues = queue.Queue()
plugroute = routespec.routeinfo
inputdata = msg.get_input_message(
msginputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes, isnoderange,
configmanager)
if 'handler' in plugroute: # fixed handler definition, easy enough
@@ -780,7 +862,7 @@ def handle_node_request(configmanager, inputdata, operation,
passvalue = hfunc(
nodes=nodes, element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata)
inputdata=msginputdata)
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
@@ -833,13 +915,13 @@ def handle_node_request(configmanager, inputdata, operation,
workers.spawn(addtoqueue, passvalues, hfunc, {'nodes': nodesbyhandler[hfunc],
'element': pathcomponents,
'configmanager': configmanager,
'inputdata': inputdata})
'inputdata': msginputdata})
for manager in nodesbymanager:
numworkers += 1
workers.spawn(addtoqueue, passvalues, dispatch_request, {
'nodes': nodesbymanager[manager], 'manager': manager,
'element': pathcomponents, 'configmanager': configmanager,
'inputdata': inputdata, 'operation': operation})
'inputdata': inputdata, 'operation': operation, 'isnoderange': isnoderange})
if isnoderange or not autostrip:
return iterate_queue(numworkers, passvalues)
else:
@@ -883,11 +965,11 @@ def addtoqueue(theq, fun, kwargs):
def dispatch_request(nodes, manager, element, configmanager, inputdata,
operation):
operation, isnoderange):
a = configmanager.get_collective_member(manager)
try:
remote = socket.create_connection((a['address'], 13001))
remote.settimeout(90)
remote.settimeout(180)
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
@@ -907,12 +989,20 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
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})
dreq = b'\x01\x03' + msgpack.packb(
{'name': myname, 'nodes': list(nodes),
'path': element,'tenant': configmanager.tenant,
'operation': operation, 'inputdata': inputdata, 'isnoderange': isnoderange}, use_bin_type=False)
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote.sendall(dreq)
while True:
@@ -959,9 +1049,14 @@ def dispatch_request(nodes, manager, element, configmanager, inputdata,
a['name']))
return
rsp += nrsp
rsp = pickle.loads(rsp)
try:
rsp = msg.msg_deserialize(rsp)
except Exception:
rsp = exc.deserialize_exc(rsp)
if isinstance(rsp, Exception):
raise rsp
if not rsp:
raise Exception('Error in cross-collective serialize/deserialze, see remote logs')
yield rsp
@@ -1005,6 +1100,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
+41 -8
View File
@@ -65,9 +65,11 @@ 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.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
@@ -89,6 +91,11 @@ import eventlet.semaphore
autosensors = set()
scanner = None
try:
unicode
except NameError:
unicode = str
class nesteddict(dict):
def __missing__(self, key):
@@ -100,23 +107,35 @@ nodehandlers = {
'service:management-hardware.Lenovo:lenovo-xclarity-controller': xcc,
'service:management-hardware.IBM:integrated-management-module2': imm,
'pxe-client': pxeh,
'onie-switch': None,
'cumulus-switch': None,
'service:io-device.Lenovo:management-module': None,
'service:thinkagile-storage': cpstorage,
'service:lenovo-tsm': tsm,
}
servicenames = {
'pxe-client': 'pxe-client',
'onie-switch': 'onie-switch',
'cumulus-switch': 'cumulus-switch',
'service:lenovo-smm': 'lenovo-smm',
'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 = {
'pxe-client': 'pxe-client',
'onie-switch': 'onie-switch',
'cumulus-switch': 'cumulus-switch',
'lenovo-smm': 'service:lenovo-smm',
'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)
@@ -363,7 +382,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:
@@ -697,7 +716,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):
@@ -881,7 +901,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'
@@ -935,9 +955,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
@@ -1056,7 +1076,7 @@ def discover_node(cfg, handler, info, nodename, manual):
traceback.print_exc()
return False
newnodeattribs = {}
if cfm.list_collective():
if list(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)
@@ -1080,6 +1100,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(
@@ -1193,12 +1217,21 @@ def rescan():
if scanner:
return
else:
scanner = eventlet.spawn(slp.active_scan, safe_detected, slp)
scanner = eventlet.spawn(blocking_scan)
def blocking_scan():
global scanner
slpscan = eventlet.spawn(slp.active_scan, safe_detected, slp)
ssdpscan = eventlet.spawn(ssdp.active_scan, safe_detected, ssdp)
slpscan.wait()
ssdpscan.wait()
scanner = None
def start_detection():
global attribwatcher
global rechecker
global rechecktime
_map_unique_ids()
cfg = cfm.ConfigManager(None)
allnodes = cfg.list_nodes()
@@ -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,37 +56,37 @@ class NodeHandler(generic.NodeHandler):
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(self, nodename, reset=False, customconfig=None):
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 customconfig:
customconfig(ic)
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,
@@ -95,6 +106,55 @@ class NodeHandler(generic.NodeHandler):
raise exc.TargetEndpointBadCredentials(
'secret.hardwaremanagementuser and/or '
'secret.hardwaremanagementpassword was not configured')
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)
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:
if uid <= lockedusers: # we cannot delete, settle for disable
ic.disable_user(uid, 'disable')
else:
# lead with the most critical thing, removing user access
ic.set_user_access(uid, channel=None, callback=False,
link_auth=False, ipmi_msg=False,
privilege_level='no_access')
# next, try to disable the password
ic.set_user_password(uid, mode='disable', password=None)
# ok, now we can be less paranoid
try:
ic.user_delete(uid)
except pygexc.IpmiException as ie:
if ie.ipmicode != 0xd5: # some response to the 0xff
# 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(
@@ -123,44 +183,6 @@ class NodeHandler(generic.NodeHandler):
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)
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')
# Now to zap others
for uid in currusers:
if uid != newuserslot:
if uid <= lockedusers: # we cannot delete, settle for disable
ic.disable_user(uid, 'disable')
else:
# lead with the most critical thing, removing user access
ic.set_user_access(uid, channel=None, callback=False,
link_auth=False, ipmi_msg=False,
privilege_level='no_access')
# next, try to disable the password
ic.set_user_password(uid, mode='disable', password=None)
# ok, now we can be less paranoid
try:
ic.user_delete(uid)
except pygexc.IpmiException as ie:
if ie.ipmicode != 0xd5: # some response to the 0xff
# name...
# the user will remain, but that is life
raise
if reset:
ic.reset_bmc()
return ic
@@ -0,0 +1,70 @@
# 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
import eventlet
import confluent.util as util
try:
from urllib import urlencode
except ImportError:
from urllib.parse import urlencode
webclient = eventlet.import_patched('pyghmi.util.webclient')
class NodeHandler(bmchandler.NodeHandler):
DEFAULT_USER = 'admin'
DEFAULT_PASS = 'admin'
devname = 'BMC'
maxmacs = 2
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, user, passwd, newuser, newpass):
wc = webclient.SecureHTTPConnection(self.ipaddr, 443,
verifycallback=self.validate_cert)
wc.connect()
authdata = urlencode({'username': user, 'password': passwd,
'weblogsign': 1})
res = wc.grab_json_response_with_status('/api/session', authdata)
if res[1] == 200:
if res[0].get('force_password', 1) == 0:
# Need to handle password change
passchange = {
'Password': newpass,
'RetypePassword': newpass,
'param': 4,
'username': 'admin',
'privilege': 4,
}
passchange = urlencode(passchange)
rsp = wc.grab_json_response_with_status('/api/reset-pass',
passchange)
rsp = wc.grab_json_response_with_status('/api/session',
method='DELETE')
def config(self, nodename, reset=False):
self.nodename = nodename
creds = self.configmanager.get_node_attributes(
self.nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'],
decrypt=True)
user, passwd, isdefault = self.get_node_credentials(
nodename, creds, 'admin', 'admin')
if not isdefault:
self.get_webclient(self.DEFAULT_USER, self.DEFAULT_PASS, user,
passwd)
self._bmcconfig(nodename, False)
@@ -57,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):
@@ -69,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,10 +12,11 @@
# 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):
@@ -38,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
@@ -40,9 +55,11 @@ class NodeHandler(bmchandler.NodeHandler):
# 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 set_password_policy(self, ic):
def _webconfigrules(self, wc):
rules = []
for rule in self.ruleset.split(','):
if '=' not in rule:
@@ -60,10 +77,102 @@ class NodeHandler(bmchandler.NodeHandler):
rules.append('passwordReuseCheckNum:' + value)
if rules:
apirequest = 'set={0}'.format(','.join(rules))
ic.register_key_handler(self._validate_cert)
ic.oem_init()
ic._oem.smmhandler.wc.request('POST', '/data', apirequest)
ic._oem.smmhandler.wc.getresponse().read()
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')
wc.request('POST', '/data', 'get=hostname')
rsp = wc.getresponse()
rspdata = fromstring(util.stringify(rsp.read()))
currip = rspdata.find('netConfig').find('ifConfigEntries').find(
'ifConfig').find('v4IPAddr').text
if currip == smmip:
return
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
@@ -71,7 +180,29 @@ class NodeHandler(bmchandler.NodeHandler):
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
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:
@@ -86,4 +217,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])
@@ -12,10 +12,31 @@
# 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()
@@ -23,27 +44,71 @@ import pyghmi.ipmi.oem.lenovo.imm as imm
class NodeHandler(immhandler.NodeHandler):
devname = 'XCC'
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):
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:
@@ -55,9 +120,123 @@ class NodeHandler(immhandler.NodeHandler):
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def set_password_policy(self, ic):
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
wc = self.wc
self.set_password_policy('', wc)
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, wc):
ruleset = {'USER_GlobalMinPassChgInt': '0'}
for rule in self.ruleset.split(','):
for rule in strruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
@@ -75,33 +254,175 @@ class NodeHandler(immhandler.NodeHandler):
ruleset['USER_GlobalPassComplexRequired'] = value
if name.lower() == 'reuse':
ruleset['USER_GlobalMinPassReuseCycle'] = value
ic.register_key_handler(self.validate_cert)
ic.oem_init()
try:
ic._oem.immhandler.wc.grab_json_response('/api/dataset', ruleset)
wc.grab_json_response('/api/dataset', ruleset)
except Exception as e:
print(repr(e))
pass
def _get_next_userid(self, wc):
userinfo = wc.grab_json_response('/api/dataset/imm_users')
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.
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
strruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
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, wc)
if self._atdefaultcreds:
if isdefault and self.tmppasswd:
raise Exception(
'Request to use default credentials, but refused by target after it has been changed to {0}'.format(self.tmppasswd))
if not isdefault:
self._setup_xcc_account(user, passwd, wc)
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..
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(
@@ -110,8 +431,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
@@ -30,33 +30,67 @@ pxearchs = {
'\x00\x07': 'uefi-x64',
'\x00\x09': 'uefi-x64',
'\x00\x0b': 'uefi-aarch64',
'\x00\x10': 'uefi-httpboot',
}
def stringify(value):
string = bytes(value)
if not isinstance(string, str):
string = string.decode('utf8')
return string
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()
def _decode_ocp_vivso(rq, idx, size):
end = idx + size
vivso = {'service-type': 'onie-switch'}
while idx < end:
if rq[idx] == 3:
vivso['machine'] = stringify(rq[idx + 2:idx + 2 + rq[idx + 1]])
elif rq[idx] == 4:
vivso['arch'] = stringify(rq[idx + 2:idx + 2 + rq[idx + 1]])
elif rq[idx] == 5:
vivso['revision'] = stringify(rq[idx + 2:idx + 2 + rq[idx + 1]])
idx += rq[idx + 1] + 2
return '', None, vivso
def find_info_in_options(rq, optidx):
uuid = None
arch = None
vivso = None
ztpurlrequested = False
iscumulus = False
try:
while uuid is None or arch is None:
if rq[optidx] == 53: # DHCP message type
# we want only length 1 and only discover (type 1)
if rq[optidx + 1] != 1 or rq[optidx + 2] != 1:
return uuid, arch
return uuid, arch, vivso
optidx += 3
elif rq[optidx] == 55:
if 239 in rq[optidx + 2:optidx + 2 + rq[optidx + 1]]:
ztpurlrequested = True
optidx += rq[optidx + 1] + 2
elif rq[optidx] == 60:
vci = stringify(rq[optidx + 2:optidx + 2 + rq[optidx + 1]])
if vci.startswith('cumulus-linux'):
iscumulus = True
arch = vci.replace('cumulus-linux', '').strip()
optidx += rq[optidx + 1] + 2
elif rq[optidx] == 97:
if rq[optidx + 1] != 17:
# 16 bytes of uuid and one reserved byte
return uuid, arch
return uuid, arch, vivso
if rq[optidx + 2] != 0: # the reserved byte should be zero,
# anything else would be a new spec that we don't know yet
return uuid, arch
return uuid, arch, vivso
uuid = decode_uuid(rq[optidx + 3:optidx + 19])
optidx += 19
elif rq[optidx] == 93:
@@ -66,11 +100,20 @@ def find_info_in_options(rq, optidx):
if archraw in pxearchs:
arch = pxearchs[archraw]
optidx += 4
elif rq[optidx] == 125:
#vivso = rq[optidx + 2:optidx + 2 + rq[optidx + 1]]
if rq[optidx + 2:optidx + 6] == b'\x00\x00\xa6\x7f': # OCP
return _decode_ocp_vivso(rq, optidx + 7, rq[optidx + 6])
optidx += rq[optidx + 1] + 2
else:
optidx += rq[optidx + 1] + 2
except IndexError:
return uuid, arch
return uuid, arch
pass
if not vivso and iscumulus and ztpurlrequested:
if not uuid:
uuid = ''
vivso = {'service-type': 'cumulus-switch', 'arch': arch}
return uuid, arch, vivso
def snoop(handler, protocol=None):
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
@@ -98,10 +141,17 @@ def snoop(handler, protocol=None):
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)
uuid, arch, vivso = find_info_in_options(rq, optidx)
if vivso:
# info['modelnumber'] = info['attributes']['enclosure-machinetype-model'][0]
handler({'hwaddr': netaddr, 'uuid': uuid,
'architecture': vivso.get('arch', ''),
'services': (vivso['service-type'],),
'attributes': {'enclosure-machinetype-model': [vivso.get('machine', '')]}})
continue
if uuid is None:
continue
# We will fill out service to have something to byte into,
@@ -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('=', 1)
attrname = attrname.decode('utf-8')
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(',')]
@@ -484,6 +493,20 @@ def snoop(handler, protocol=None):
_add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
peerbymacaddress[mac]['protocol'] = protocol
for srvurl in peerbymacaddress[mac].get('urls', ()):
if len(srvurl) > 4:
srvurl = srvurl[:-3]
if srvurl.endswith('://Athena:'):
continue
if 'service:ipmi' in peerbymacaddress[mac]['services']:
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,
@@ -547,6 +570,13 @@ 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:
for srvurl in rsps[id].get('urls', ()):
if len(srvurl) > 4:
srvurl = srvurl[:-3]
if srvurl.endswith('://Athena:'):
continue
if 'service:ipmi' in rsps[id]['services']:
continue
if localonly:
for addr in rsps[id]['addresses']:
if 'fe80' in addr[0]:
@@ -554,6 +584,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']
@@ -32,6 +32,10 @@ import confluent.neighutil as neighutil
import confluent.util as util
import eventlet.green.select as select
import eventlet.green.socket as socket
try:
from eventlet.green.urllib.request import urlopen
except (ImportError, AssertionError):
from eventlet.green.urllib2 import urlopen
import struct
mcastv4addr = '239.255.255.250'
@@ -45,6 +49,20 @@ smsg = ('M-SEARCH * HTTP/1.1\r\n'
'MX: 3\r\n\r\n')
def active_scan(handler, protocol=None):
known_peers = set([])
for scanned in scan(['urn:dmtf-org:service:redfish-rest:1']):
for addr in scanned['addresses']:
ip = addr[0].partition('%')[0] # discard scope if present
if ip not in neighutil.neightable:
continue
if addr in known_peers:
break
known_peers.add(addr)
else:
scanned['protocol'] = protocol
handler(scanned)
def scan(services, target=None):
for service in services:
for rply in _find_service(service, target):
@@ -78,8 +96,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))
@@ -104,11 +127,11 @@ def snoop(handler, byehandler=None):
known_peers.add(peer)
newmacs.add(mac)
if mac in peerbymacaddress:
peerbymacaddress[mac]['peers'].append(peer)
peerbymacaddress[mac]['addresses'].append(peer)
else:
peerbymacaddress[mac] = {
'hwaddr': mac,
'peers': [peer],
'addresses': [peer],
}
peerdata = peerbymacaddress[mac]
for headline in rsp[1:]:
@@ -180,7 +203,12 @@ def _find_service(service, target):
timeout = 0
r, _, _ = select.select((net4, net6), (), (), timeout)
for nid in peerdata:
yield peerdata[nid]
for url in peerdata[nid].get('urls', ()):
if url.endswith('/desc.tmpl'):
info = urlopen(url).read()
if '<friendlyName>Athena</friendlyName>' in info:
peerdata[nid]['services'] = ['service:thinkagile-storage']
yield peerdata[nid]
def _parse_ssdp(peer, rsp, peerdata):
@@ -198,11 +226,11 @@ 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)
if peer not in peerdatum['addresses']:
peerdatum['addresses'].append(peer)
else:
peerdatum = {
'peers': [peer],
'addresses': [peer],
'hwaddr': mac,
}
peerdata[nid] = peerdatum
+23 -1
View File
@@ -17,7 +17,17 @@
import base64
import json
import msgpack
def deserialize_exc(msg):
excd = msgpack.unpackb(msg, raw=False)
if excd[0] == 'Exception':
return Exception(excd[1])
if excd[0] not in globals():
return Exception('Cannot deserialize: {0}'.format(repr(excd)))
if not issubclass(excd[0], ConfluentException):
return Exception('Cannot deserialize: {0}'.format(repr(excd)))
return globals(excd[0])(*excd[1])
class ConfluentException(Exception):
apierrorcode = 500
@@ -27,6 +37,10 @@ class ConfluentException(Exception):
errstr = ' - '.join((self._apierrorstr, str(self)))
return json.dumps({'error': errstr })
def serialize(self):
return msgpack.packb([self.__class__.__name__, [str(self)]],
use_bin_type=False)
@property
def apierrorstr(self):
if str(self):
@@ -104,16 +118,24 @@ class PubkeyInvalid(ConfluentException):
def __init__(self, text, certificate, fingerprint, attribname, event):
super(PubkeyInvalid, self).__init__(self, text)
self.myargs = (text, certificate, fingerprint, attribname, event)
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 serialize(self):
return msgpack.packb([self.__class__.__name__, self.myargs],
use_bin_type=False)
def get_error_body(self):
return self.errorbody
+27 -7
View File
@@ -36,20 +36,40 @@ _tracelog = None
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':
errstr = False
if not os.path.exists(filename):
errstr = '{0} does not appear to exist on {1}'.format(
filename, socket.gethostname())
elif not os.access(filename, os.R_OK):
errstr = '{0} is not readable by confluent on {1} (ensure confluent user or group can access file and parent directories)'.format(
filename, socket.gethostname())
if errstr:
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
return
if type == 'ffdc' and os.path.isdir(filename):
filename += '/' + node + '.svcdata'
filename += '/' + node
if 'type' == 'ffdc':
errstr = False
if os.path.exists(filename):
errstr = '{0} already exists on {1}, cannot overwrite'.format(
filename, socket.gethostname())
elif not os.access(os.path.dirname(filename), os.W_OK):
errstr = '{0} directory not writable by confluent user/group on {1}, check the directory and parent directory ownership and permissions'.format(filename, socket.gethostname())
if errstr:
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
return
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:
+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
+57 -39
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
@@ -410,7 +416,7 @@ def resourcehandler_backend(env, start_response):
reqtype = env['CONTENT_TYPE']
operation = opmap[env['REQUEST_METHOD']]
querydict = _get_query_dict(env, reqbody, reqtype)
if 'restexplorerop' in querydict:
if operation != 'retrieve' and 'restexplorerop' in querydict:
operation = querydict['restexplorerop']
del querydict['restexplorerop']
authorized = _authorize_request(env, operation)
@@ -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']
@@ -499,10 +511,10 @@ def resourcehandler_backend(env, start_response):
width = querydict.get('width', 80)
height = querydict.get('height', 24)
datacallback = None
async = None
asynchdl = None
if 'HTTP_CONFLUENTASYNCID' in env:
async = confluent.asynchttp.get_async(env, querydict)
termrel = async.set_term_relation(env)
asynchdl = confluent.asynchttp.get_async(env, querydict)
termrel = asynchdl.set_term_relation(env)
datacallback = termrel.got_data
try:
if shellsession:
@@ -525,8 +537,8 @@ def resourcehandler_backend(env, start_response):
start_response("500 Internal Server Error", headers)
return
sessid = _assign_consessionid(consession)
if async:
async.add_console_session(sessid)
if asynchdl:
asynchdl.add_console_session(sessid)
start_response('200 OK', headers)
yield '{"session":"%s","data":""}' % sessid
return
@@ -622,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('/'):]
@@ -643,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
@@ -727,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'])
@@ -743,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):
@@ -762,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):
@@ -790,7 +805,10 @@ def serve(bind_host, bind_port):
' a second\n')
eventlet.sleep(1)
# TCP_FASTOPEN
sock.setsockopt(socket.SOL_TCP, 23, 5)
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)
+45 -24
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,6 +76,10 @@ import stat
import struct
import time
import traceback
try:
unicode
except NameError:
unicode = str
daemonized = False
logfull = False
@@ -172,6 +180,8 @@ class BaseRotatingHandler(object):
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()
@@ -576,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.
@@ -731,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:
@@ -781,4 +802,4 @@ def logtrace():
if tracelog is None:
tracelog = Logger('trace', buffered=False)
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
event=Events.stacktrace)
+14 -6
View File
@@ -43,9 +43,11 @@ except ImportError:
import confluent.discovery.core as disco
import eventlet
dbgif = False
if map(int, (eventlet.__version__.split('.'))) > [0, 18]:
try:
import eventlet.backdoor as backdoor
dbgif = True
except Exception:
pass
havefcntl = True
try:
import fcntl
@@ -75,13 +77,16 @@ def _daemonize():
print('confluent server starting as pid {0}'.format(thispid))
os._exit(0)
os.closerange(0, 2)
os.umask(63)
os.open(os.devnull, os.O_RDWR)
os.dup2(0, 1)
os.dup2(0, 2)
log.daemonized = True
def _redirectoutput():
os.umask(63)
sys.stdout = log.Logger('stdout', buffered=False)
sys.stderr = log.Logger('stderr', buffered=False)
log.daemonized = True
def _updatepidfile():
@@ -204,7 +209,7 @@ def setlimits():
pass
def run():
def run(args):
setlimits()
try:
signal.signal(signal.SIGUSR1, dumptrace)
@@ -230,14 +235,17 @@ def run():
except (OSError, IOError) as e:
print(repr(e))
sys.exit(1)
_daemonize()
if '-f' not in args:
_daemonize()
if '-o' not in args:
_redirectoutput()
if havefcntl:
_updatepidfile()
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
collective.startup()
if dbgif:
oumask = os.umask(0077)
oumask = os.umask(0o077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
+226 -49
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# 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.
@@ -20,10 +20,18 @@
# format. This is also how different data formats are supported
import confluent.exceptions as exc
import confluent.config.configmanager as cfm
import confluent.config.conf as cfgfile
from copy import deepcopy
from datetime import datetime
import confluent.util as util
import msgpack
import json
try:
unicode
except NameError:
unicode = str
valid_health_values = set([
'ok',
'warning',
@@ -32,6 +40,17 @@ valid_health_values = set([
'unknown',
])
passcomplexity = cfgfile.get_option('policy', 'passwordcomplexity')
passminlength = cfgfile.get_option('policy', 'passwordminlength')
if passminlength:
passminlength = int(passminlength)
else:
passminlength = 0
if passcomplexity:
passcomplexity = int(passcomplexity)
else:
passcomplexity = 0
def simplify_name(name):
return name.lower().replace(' ', '_').replace('/', '-').replace(
'_-_', '-')
@@ -42,7 +61,9 @@ def _htmlify_structure(indict):
if isinstance(indict, dict):
for key in sorted(indict):
ret += "<li>{0}: ".format(key)
if type(indict[key]) in (str, unicode, float, int):
if type(indict[key]) in (bytes, unicode):
ret += util.stringify(indict[key])
if type(indict[key]) in (float, int):
ret += str(indict[key])
elif isinstance(indict[key], datetime):
ret += indict[key].strftime('%Y-%m-%dT%H:%M:%S')
@@ -50,13 +71,13 @@ def _htmlify_structure(indict):
ret += _htmlify_structure(indict[key])
elif isinstance(indict, list):
if len(indict) > 0:
if type(indict[0]) in (str, unicode, None):
if type(indict[0]) in (bytes, unicode, None):
nd = []
for datum in indict:
if datum is None:
nd.append('')
else:
nd.append(datum)
nd.append(util.stringify(datum))
ret += ",".join(nd)
else:
for v in indict:
@@ -64,6 +85,13 @@ def _htmlify_structure(indict):
return ret + '</ul>'
def msg_deserialize(packed):
m = msgpack.unpackb(packed, raw=False)
cls = globals()[m[0]]
if issubclass(cls, ConfluentMessage) or issubclass(cls, ConfluentNodeError):
return cls(*m[1:])
raise Exception("Unknown shenanigans")
class ConfluentMessage(object):
apicode = 200
readonly = False
@@ -85,6 +113,15 @@ class ConfluentMessage(object):
jsonsnippet = json.dumps(datasource, sort_keys=True, separators=(',', ':'))[1:-1]
return jsonsnippet
def serialize(self):
msg = [self.__class__.__name__]
msg.extend(self.myargs)
return msgpack.packb(msg, use_bin_type=False)
@classmethod
def deserialize(cls, data):
return cls(*data)
def raw(self):
"""Return pythonic representation of the response.
@@ -109,15 +146,16 @@ class ConfluentMessage(object):
return self._generic_html_value(self.kvpairs)
if not self.stripped:
htmlout = ''
for node in self.kvpairs.iterkeys():
for node in self.kvpairs:
htmlout += '{0}:{1}\n'.format(
node, self._generic_html_value(self.kvpairs[node]))
return htmlout
def _generic_html_value(self, pairs):
snippet = ""
for key in pairs.iterkeys():
for key in pairs:
val = pairs[key]
key = util.stringify(key)
value = self.defaultvalue
if isinstance(val, dict) and 'type' in val:
valtype = val['type']
@@ -144,7 +182,7 @@ class ConfluentMessage(object):
'<input type="checkbox" name="restexplorerhonorkey" '
'value="{1}">\r').format(valtype, key, self.desc)
return snippet
if (isinstance(val, bool) or isinstance(val, str) or
if (isinstance(val, bool) or isinstance(val, bytes) or
isinstance(val, unicode)):
value = str(val)
elif val is not None and 'value' in val:
@@ -190,6 +228,15 @@ class ConfluentNodeError(object):
self.node = node
self.error = errorstr
def serialize(self):
return msgpack.packb(
[self.__class__.__name__, self.node, self.error],
use_bin_type=False)
@classmethod
def deserialize(cls, data):
return cls(*data)
def raw(self):
return {'databynode': {self.node: {'errorcode': self.apicode,
'error': self.error}}}
@@ -200,7 +247,7 @@ class ConfluentNodeError(object):
def strip_node(self, node):
# NOTE(jjohnson2): For single node errors, raise exception to
# trigger what a developer of that medium would expect
raise Exception(self.error)
raise Exception('{0}: {1}'.format(self.node, self.error))
class ConfluentResourceUnavailable(ConfluentNodeError):
@@ -239,9 +286,9 @@ class ConfluentTargetNotFound(ConfluentNodeError):
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
apicode = 502
def __init__(self, node):
def __init__(self, node, errstr='bad credentials'):
self.node = node
self.error = 'bad credentials'
self.error = errstr
def strip_node(self, node):
raise exc.TargetEndpointBadCredentials
@@ -250,6 +297,7 @@ class ConfluentTargetInvalidCredentials(ConfluentNodeError):
class DeletedResource(ConfluentMessage):
notnode = True
def __init__(self, resource):
self.myargs = [resource]
self.kvpairs = {'deleted': resource}
def strip_node(self, node):
@@ -261,6 +309,7 @@ class CreatedResource(ConfluentMessage):
readonly = True
def __init__(self, resource):
self.myargs = [resource]
self.kvpairs = {'created': resource}
def strip_node(self, node):
@@ -272,6 +321,7 @@ class RenamedResource(ConfluentMessage):
readonly = True
def __init__(self, oldname, newname):
self.myargs = (oldname, newname)
self.kvpairs = {'oldname': oldname, 'newname': newname}
def strip_node(self, node):
@@ -280,6 +330,7 @@ class RenamedResource(ConfluentMessage):
class RenamedNode(ConfluentMessage):
def __init__(self, name, rename):
self.myargs = (name, rename)
self.desc = 'New Name'
kv = {'rename': {'value': rename}}
self.kvpairs = {name: kv}
@@ -290,13 +341,16 @@ class AssignedResource(ConfluentMessage):
readonly = True
def __init__(self, resource):
self.myargs = [resource]
self.kvpairs = {'assigned': resource}
class ConfluentChoiceMessage(ConfluentMessage):
valid_values = set()
valid_paramset = {}
def __init__(self, node, state):
self.myargs = (node, state)
self.stripped = False
self.kvpairs = {
node: {
@@ -309,15 +363,16 @@ class ConfluentChoiceMessage(ConfluentMessage):
return self._create_option(self.kvpairs)
else:
htmlout = ''
for node in self.kvpairs.iterkeys():
for node in self.kvpairs:
htmlout += '{0}:{1}\n'.format(
node, self._create_option(self.kvpairs[node]))
return htmlout
def _create_option(self, pairdata):
snippet = ''
for key in pairdata.iterkeys():
for key in pairdata:
val = pairdata[key]
key = util.stringify(key)
snippet += key + ':<select name="%s">' % key
valid_values = self.valid_values
if key in self.valid_paramset:
@@ -369,6 +424,7 @@ class LinkRelation(ConfluentMessage):
class ChildCollection(LinkRelation):
def __init__(self, collname, candelete=False):
self.myargs = (collname, candelete)
self.rel = 'item'
self.href = collname
self.candelete = candelete
@@ -401,7 +457,7 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputExpression(path, inputdata, nodes)
elif path == ['attributes', 'rename']:
return InputConfigChangeSet(path, inputdata, nodes, configmanager)
elif path[0] in ('attributes', 'users') and operation != 'retrieve':
elif path[0] in ('attributes', 'users', 'usergroups') and operation != 'retrieve':
return InputAttributes(path, inputdata, nodes)
elif path == ['boot', 'nextdevice'] and operation != 'retrieve':
return InputBootDevice(path, nodes, inputdata)
@@ -438,10 +494,11 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
'servers'] and operation != 'retrieve' and len(path) == 5):
return InputNTPServer(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'system', 'all'] and
elif (path[:3] in (['configuration', 'system', 'all'],
['configuration', 'management_controller', 'extended']) and
operation != 'retrieve'):
return InputConfigChangeSet(path, inputdata, nodes, configmanager)
elif (path[:3] == ['configuration', 'system', 'clear'] and
elif (path[0] == 'configuration' and path[2] == 'clear' and
operation != 'retrieve'):
return InputConfigClear(path, inputdata, nodes, configmanager)
elif (path[:3] == ['configuration', 'storage', 'disks'] and
@@ -458,6 +515,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
return InputMedia(path, nodes, inputdata, configmanager)
elif '/'.join(path).startswith('support/servicedata') and inputdata:
return InputMedia(path, nodes, inputdata, configmanager)
elif '/'.join(path).startswith('configuration/management_controller/save_licenses') and inputdata:
return InputMedia(path, nodes, inputdata, configmanager)
elif '/'.join(path).startswith(
'configuration/management_controller/licenses') and inputdata:
return InputLicense(path, nodes, inputdata, configmanager)
@@ -468,7 +527,7 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
class InputFirmwareUpdate(ConfluentMessage):
def __init__(self, path, nodes, inputdata, configmanager):
self._filename = inputdata.get('filename', inputdata.get('url', None))
self._filename = inputdata.get('filename', inputdata.get('url', inputdata.get('dirname', None)))
self.bank = inputdata.get('bank', None)
self.nodes = nodes
self.filebynode = {}
@@ -486,9 +545,21 @@ class InputFirmwareUpdate(ConfluentMessage):
raise Exception('User requested substitutions, but code is '
'written against old api, code must be fixed or '
'skip {} expansion')
if self.filebynode[node].startswith('/etc/confluent'):
raise Exception(
'File transfer with /etc/confluent is not supported')
if self.filebynode[node].startswith('/var/log/confluent'):
raise Exception(
'File transfer with /var/log/confluent is not supported')
return self._filename
def nodefile(self, node):
if self.filebynode[node].startswith('/etc/confluent'):
raise Exception(
'File transfer with /etc/confluent is not supported')
if self.filebynode[node].startswith('/var/log/confluent'):
raise Exception(
'File transfer with /var/log/confluent is not supported')
return self.filebynode[node]
class InputMedia(InputFirmwareUpdate):
@@ -507,8 +578,16 @@ class DetachMedia(ConfluentMessage):
class Media(ConfluentMessage):
def __init__(self, node, media):
self.kvpairs = {node: {'name': media.name, 'url': media.url}}
def __init__(self, node, media=None, rawmedia=None):
if media:
rawmedia = {'name': media.name, 'url': media.url}
self.myargs = (node, None, rawmedia)
self.kvpairs = {node: rawmedia}
class SavedFile(ConfluentMessage):
def __init__(self, node, file):
self.myargs = (node, file)
self.kvpairs = {node: {'filename': file}}
class InputAlertData(ConfluentMessage):
@@ -568,7 +647,7 @@ class InputConfigChangeSet(InputExpression):
endattrs = {}
for attr in attrs:
origval = attrs[attr]
if isinstance(origval, str) or isinstance(origval, unicode):
if isinstance(origval, bytes) or isinstance(origval, unicode):
origval = {'expression': origval}
if 'expression' not in origval:
endattrs[attr] = attrs[attr]
@@ -595,7 +674,9 @@ class InputAttributes(ConfluentMessage):
if nodes is None:
self.attribs = inputdata
for attrib in self.attribs:
if type(self.attribs[attrib]) in (str, unicode):
if not cfm.attrib_supports_expression(attrib):
continue
if type(self.attribs[attrib]) in (bytes, unicode):
try:
# ok, try to use format against the string
# store back result to the attribute to
@@ -621,7 +702,7 @@ class InputAttributes(ConfluentMessage):
return {}
nodeattr = deepcopy(self.nodeattribs[node])
for attr in nodeattr:
if type(nodeattr[attr]) in (str, unicode):
if type(nodeattr[attr]) in (bytes, unicode) and cfm.attrib_supports_expression(attr):
try:
# as above, use format() to see if string follows
# expression, store value back in case of escapes
@@ -662,13 +743,51 @@ class InputAttributes(ConfluentMessage):
)
return nodeattr
def checkPassword(password, username):
lowercase = set('abcdefghijklmnopqrstuvwxyz')
uppercase = set('abcdefghijklmnopqrstuvwxyz'.upper())
numbers = set('0123456789')
special = set('`~!@#$%^&*()-_=+[{]};:"/?.>,<' + "'")
if len(password) < passminlength:
raise exc.InvalidArgumentException('Password must be at least {0} characters long'.format(passminlength))
if not isinstance(passcomplexity, int) or passcomplexity < 1:
return
if not bool(set(password.lower()) & lowercase): # rule 1
raise exc.InvalidArgumentException('Password must contain at least one letter')
if passcomplexity < 2:
return
thepass = set(password)
if not bool(thepass & numbers): # rule 2
raise exc.InvalidArgumentException('Password must contain at least one number')
if passcomplexity < 3:
return
classes = 0
for charclass in (lowercase, uppercase, special):
if bool(thepass & charclass):
classes += 1
if classes < 2:
raise exc.InvalidArgumentException('Password must contain at least two of upper case letter, lower case letter, and/or special character')
if passcomplexity < 4:
return
if username and password in (username, username[::-1]): # rule 4
raise exc.InvalidArgumentException('Password must not be similar to username')
if passcomplexity < 5:
return
for char in thepass:
if char * 3 in password:
raise exc.InvalidArgumentException('Password must not contain any of the same character repeated 3 times')
class InputCredential(ConfluentMessage):
valid_privilege_levels = set([
'callback',
'user',
'ReadOnly',
'operator',
'Operator',
'administrator',
'Administrator',
'proprietary',
'no_access',
])
@@ -686,33 +805,21 @@ class InputCredential(ConfluentMessage):
if len(path) == 4:
inputdata['uid'] = path[-1]
# if the operation is 'create' check if all fields are present
missingattrs = []
for attrname in ('uid', 'privilege_level', 'username', 'password'):
if attrname not in inputdata:
missingattrs.append(attrname)
if missingattrs:
raise exc.InvalidArgumentException(
'Required fields missing: {0}'.format(','.join(missingattrs)))
if (isinstance(inputdata['uid'], str) and
if (type(inputdata['uid']) in (bytes, unicode) and
not inputdata['uid'].isdigit()):
raise exc.InvalidArgumentException('uid must be a number')
inputdata['uid'] = inputdata['uid']
else:
inputdata['uid'] = int(inputdata['uid'])
if ('privilege_level' in inputdata and
inputdata['privilege_level'] not in self.valid_privilege_levels):
raise exc.InvalidArgumentException('privilege_level is not one of '
+ ','.join(self.valid_privilege_levels))
if 'username' in inputdata and len(inputdata['username']) > 16:
raise exc.InvalidArgumentException(
'name must be less than or = 16 chars')
if 'password' in inputdata and len(inputdata['password']) > 20:
raise exc.InvalidArgumentException('password has limit of 20 chars')
if ('enabled' in inputdata and
inputdata['enabled'] not in self.valid_enabled_values):
raise exc.InvalidArgumentException('valid values for enabled are '
+ 'yes and no')
if 'password' in inputdata and (passcomplexity or passminlength):
checkPassword(inputdata['password'], inputdata.get('username', None))
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
@@ -724,7 +831,7 @@ class InputCredential(ConfluentMessage):
return {}
credential = deepcopy(self.credentials[node])
for attr in credential:
if type(credential[attr]) in (str, unicode):
if type(credential[attr]) in (bytes, unicode):
try:
# as above, use format() to see if string follows
# expression, store value back in case of escapes
@@ -778,6 +885,7 @@ class InputIdentifyMessage(ConfluentInputMessage):
valid_values = set([
'on',
'off',
'blink',
])
keyname = 'identify'
@@ -809,6 +917,9 @@ class InputVolumes(ConfluentInputMessage):
sizes = inputdata.get('size', [None])
if not isinstance(sizes, list):
sizes = sizes.split(',')
stripsizes = inputdata.get('stripsizes', [None])
if not isinstance(stripsizes, list):
stripsizes = stripsizes.split(',')
disks = inputdata.get('disks', [])
if not disks:
raise exc.InvalidArgumentException(
@@ -820,8 +931,15 @@ class InputVolumes(ConfluentInputMessage):
currname = volnames.pop(0)
else:
currname = None
if stripsizes:
currstripsize = stripsizes.pop(0)
if currstripsize:
currstripsize = int(currstripsize)
else:
currstripsize = None
inputdata.append(
{'name': currname, 'size': size,
'stripsize': currstripsize,
'disks': disks,
'raidlevel': raidlvl})
for node in nodes:
@@ -843,6 +961,7 @@ class InputVolumes(ConfluentInputMessage):
self.inputbynode[node].append({'name': volname,
'size': volsize,
'disks': disks,
'stripsize': input.get('stripsize', None),
'raidlevel': raidlvl,
})
@@ -1041,6 +1160,7 @@ class BootDevice(ConfluentChoiceMessage):
}
def __init__(self, node, device, bootmode='unspecified', persistent=False):
self.myargs = (node, device, bootmode, persistent)
if device not in self.valid_values:
raise Exception("Invalid boot device argument passed in:" +
repr(device))
@@ -1139,10 +1259,10 @@ class PowerState(ConfluentChoiceMessage):
def __init__(self, node, state, oldstate=None):
super(PowerState, self).__init__(node, state)
self.myargs = (node, state, oldstate)
if oldstate is not None:
self.kvpairs[node]['oldstate'] = {'value': oldstate}
class BMCReset(ConfluentChoiceMessage):
valid_values = set([
'reset',
@@ -1158,13 +1278,13 @@ class NTPEnabled(ConfluentChoiceMessage):
def __init__(self, node, enabled):
self.stripped = False
self.myargs = (node, enabled)
self.kvpairs = {
node: {
'state': {'value': str(enabled)},
}
}
class EventCollection(ConfluentMessage):
"""A collection of events
@@ -1184,6 +1304,8 @@ class EventCollection(ConfluentMessage):
def __init__(self, events=(), name=None):
eventdata = []
self.notnode = name is None
self.myname = name
self.myargs = (eventdata, name)
for event in events:
entry = {
'id': event.get('id', None),
@@ -1192,7 +1314,9 @@ class EventCollection(ConfluentMessage):
'event': event.get('event', None),
'severity': event['severity'],
'timestamp': event.get('timestamp', None),
'message': event.get('message', None),
'record_id': event.get('record_id', None),
'log_id': event.get('log_id', None),
}
if event['severity'] not in valid_health_values:
raise exc.NotImplementedException(
@@ -1209,6 +1333,10 @@ class AsyncCompletion(ConfluentMessage):
self.stripped = True
self.notnode = True
@classmethod
def deserialize(cls):
raise Exception("Not supported")
def raw(self):
return {'_requestdone': True}
@@ -1219,6 +1347,10 @@ class AsyncMessage(ConfluentMessage):
self.notnode = True
self.msgpair = pair
@classmethod
def deserialize(cls):
raise Exception("Not supported")
def raw(self):
rsp = self.msgpair[1]
rspdict = None
@@ -1246,14 +1378,16 @@ class AsyncSession(ConfluentMessage):
self.kvpairs = {'asyncid': id}
class User(ConfluentMessage):
def __init__(self, uid, username, privilege_level, name=None):
def __init__(self, uid, username, privilege_level, name=None, expiration=None):
self.desc = 'foo'
self.stripped = False
self.notnode = name is None
self.myargs = (uid, username, privilege_level, name, expiration)
kvpairs = {'username': {'value': username},
'password': {'value': '', 'type': 'password'},
'privilege_level': {'value': privilege_level},
'enabled': {'value': ''}
'enabled': {'value': ''},
'expiration': {'value': expiration},
}
if self.notnode:
self.kvpairs = kvpairs
@@ -1268,10 +1402,15 @@ class UserCollection(ConfluentMessage):
self.notnode = name is None
self.desc = 'list of users'
userlist = []
self.myargs = (userlist, name)
for user in users:
if 'username' in user: # processing an already translated dict
userlist.append(user)
continue
entry = {
'uid': user['uid'],
'username': user['name'],
'expiration': user.get('expiration', None),
'privilege_level': user['access']['privilege_level']
}
userlist.append(entry)
@@ -1281,8 +1420,10 @@ class UserCollection(ConfluentMessage):
self.kvpairs = {name: {'users': userlist}}
class AlertDestination(ConfluentMessage):
def __init__(self, ip, acknowledge=False, acknowledge_timeout=None, retries=0, name=None):
self.myargs = (ip, acknowledge, acknowledge_timeout, retries, name)
self.desc = 'foo'
self.stripped = False
self.notnode = name is None
@@ -1298,7 +1439,7 @@ class AlertDestination(ConfluentMessage):
class InputAlertDestination(ConfluentMessage):
valid_alert_params = {
'acknowledge': lambda x: False if type(x) in (unicode,str) and x.lower() == 'false' else bool(x),
'acknowledge': lambda x: False if type(x) in (unicode, bytes) and x.lower() == 'false' else bool(x),
'acknowledge_timeout': lambda x: int(x) if x and x.isdigit() else None,
'ip': lambda x: x,
'retries': lambda x: int(x)
@@ -1308,6 +1449,8 @@ class InputAlertDestination(ConfluentMessage):
self.alertcfg = {}
if multinode: # keys are node names
for node in inputdata:
if not isinstance(inputdata[node], dict):
break
self.alertcfg[node] = inputdata[node]
for key in inputdata[node]:
if key not in self.valid_alert_params:
@@ -1320,7 +1463,8 @@ class InputAlertDestination(ConfluentMessage):
else:
self.alertcfg[node][key] = \
self.valid_alert_params[key](inputdata[node][key])
else:
else:
return
for key in inputdata:
if key not in self.valid_alert_params:
raise exc.InvalidArgumentException(
@@ -1344,7 +1488,11 @@ class SensorReadings(ConfluentMessage):
def __init__(self, sensors=(), name=None):
readings = []
self.notnode = name is None
self.myargs = (readings, name)
for sensor in sensors:
if isinstance(sensor, dict):
readings.append(sensor)
continue
sensordict = {'name': sensor.name}
if hasattr(sensor, 'value'):
sensordict['value'] = sensor.value
@@ -1369,6 +1517,13 @@ class Firmware(ConfluentMessage):
readonly = True
def __init__(self, data, name):
for datum in data:
for component in datum:
for field in datum[component]:
tdatum = datum[component]
if isinstance(tdatum[field], datetime):
tdatum[field] = tdatum[field].strftime('%Y-%m-%dT%H:%M:%S')
self.myargs = (data, name)
self.notnode = name is None
self.desc = 'Firmware information'
if self.notnode:
@@ -1381,6 +1536,7 @@ class KeyValueData(ConfluentMessage):
readonly = True
def __init__(self, kvdata, name=None):
self.myargs = (kvdata, name)
self.notnode = name is None
if self.notnode:
self.kvpairs = kvdata
@@ -1390,6 +1546,7 @@ class KeyValueData(ConfluentMessage):
class Array(ConfluentMessage):
def __init__(self, name, disks=None, raid=None, volumes=None,
id=None, capacity=None, available=None):
self.myargs = (name, disks, raid, volumes, id, capacity, available)
self.kvpairs = {
name: {
'type': 'array',
@@ -1403,12 +1560,14 @@ class Array(ConfluentMessage):
}
class Volume(ConfluentMessage):
def __init__(self, name, volname, size, state, array):
def __init__(self, name, volname, size, state, array, stripsize=None):
self.myargs = (name, volname, size, state, array, stripsize)
self.kvpairs = {
name: {
'type': 'volume',
'name': simplify_name(volname),
'label': volname,
'stripsize': stripsize,
'size': size,
'state': state,
'array': array,
@@ -1417,14 +1576,18 @@ class Volume(ConfluentMessage):
class Disk(ConfluentMessage):
valid_states = set([
'fault',
'jbod',
'unconfigured',
'hotspare',
'rebuilding',
'online',
])
state_aliases = {
'unconfigured bad': 'fault',
'unconfigured good': 'unconfigured',
'global hot spare': 'hotspare',
'dedicated hot spare': 'hotspare',
}
def _normalize_state(self, instate):
@@ -1439,6 +1602,8 @@ class Disk(ConfluentMessage):
def __init__(self, name, label=None, description=None,
diskid=None, state=None, serial=None, fru=None,
array=None):
self.myargs = (name, label, description, diskid, state,
serial, fru, array)
state = self._normalize_state(state)
self.kvpairs = {
name: {
@@ -1460,6 +1625,7 @@ class LEDStatus(ConfluentMessage):
readonly = True
def __init__(self, data, name):
self.myargs = (data, name)
self.notnode = name is None
self.desc = 'led status'
@@ -1474,6 +1640,7 @@ class NetworkConfiguration(ConfluentMessage):
def __init__(self, name=None, ipv4addr=None, ipv4gateway=None,
ipv4cfgmethod=None, hwaddr=None):
self.myargs = (name, ipv4addr, ipv4gateway, ipv4cfgmethod, hwaddr)
self.notnode = name is None
self.stripped = False
@@ -1494,6 +1661,7 @@ class HealthSummary(ConfluentMessage):
valid_values = valid_health_values
def __init__(self, health, name=None):
self.myargs = (health, name)
self.stripped = False
self.notnode = name is None
if health not in self.valid_values:
@@ -1506,11 +1674,12 @@ class HealthSummary(ConfluentMessage):
class Attributes(ConfluentMessage):
def __init__(self, name=None, kv=None, desc=''):
self.myargs = (name, kv, desc)
self.desc = desc
nkv = {}
self.notnode = name is None
for key in kv:
if type(kv[key]) in (str, unicode):
if type(kv[key]) in (bytes, unicode):
nkv[key] = {'value': kv[key]}
else:
nkv[key] = kv[key]
@@ -1526,6 +1695,7 @@ class ConfigSet(Attributes):
class ListAttributes(ConfluentMessage):
def __init__(self, name=None, kv=None, desc=''):
self.myargs = (name, kv, desc)
self.desc = desc
self.notnode = name is None
if self.notnode:
@@ -1536,6 +1706,7 @@ class ListAttributes(ConfluentMessage):
class MCI(ConfluentMessage):
def __init__(self, name=None, mci=None):
self.myargs = (name, mci)
self.notnode = name is None
self.desc = 'BMC identifier'
@@ -1548,6 +1719,7 @@ class MCI(ConfluentMessage):
class Hostname(ConfluentMessage):
def __init__(self, name=None, hostname=None):
self.myargs = (name, hostname)
self.notnode = name is None
self.desc = 'BMC hostname'
@@ -1559,6 +1731,7 @@ class Hostname(ConfluentMessage):
class DomainName(ConfluentMessage):
def __init__(self, name=None, dn=None):
self.myargs = (name, dn)
self.notnode = name is None
self.desc = 'BMC domain name'
@@ -1573,6 +1746,7 @@ class NTPServers(ConfluentMessage):
readonly = True
def __init__(self, name=None, servers=None):
self.myargs = (name, servers)
self.notnode = name is None
self.desc = 'NTP Server'
@@ -1587,6 +1761,7 @@ class NTPServers(ConfluentMessage):
class NTPServer(ConfluentMessage):
def __init__(self, name=None, server=None):
self.myargs = (name, server)
self.notnode = name is None
self.desc = 'NTP Server'
@@ -1602,12 +1777,13 @@ class NTPServer(ConfluentMessage):
class License(ConfluentMessage):
readonly = True
def __init__(self, name=None, kvm=None, feature=None):
def __init__(self, name=None, kvm=None, feature=None, state=None):
self.myargs = (name, kvm, feature, state)
self.notnode = name is None
self.desc = 'License'
kv = []
kv.append({'kvm_availability': str(kvm), 'feature': feature})
kv.append({'kvm_availability': str(kvm), 'feature': feature, 'state': state})
if self.notnode:
self.kvpairs = {'License': kv}
else:
@@ -1618,10 +1794,11 @@ class CryptedAttributes(Attributes):
defaulttype = 'password'
def __init__(self, name=None, kv=None, desc=''):
self.myargs = (name, kv, desc)
# for now, just keep the dictionary keys and discard crypt value
self.desc = desc
nkv = {}
for key in kv.iterkeys():
for key in kv:
nkv[key] = {'isset': False}
try:
if kv[key] is not None and kv[key]['cryptvalue'] != '':
+2
View File
@@ -18,6 +18,7 @@
# Ultimately, this should use AF_NETLINK, but in the interest of time,
# use ip neigh for the moment
import confluent.util as util
import eventlet.green.subprocess as subprocess
import os
@@ -39,6 +40,7 @@ def update_neigh():
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(neighdata, err) = ipn.communicate()
neighdata = util.stringify(neighdata)
for entry in neighdata.split('\n'):
entry = entry.split(' ')
if len(entry) < 5 or not entry[4]:
+13
View File
@@ -24,6 +24,19 @@ import eventlet.support.greendns
getaddrinfo = eventlet.support.greendns.getaddrinfo
def mask_to_cidr(mask):
maskn = socket.inet_pton(socket.AF_INET, mask)
maskn = struct.unpack('!I', maskn)[0]
cidr = 32
while maskn & 0b1 == 0 and cidr > 0:
cidr -= 1
maskn >>= 1
return cidr
def cidr_to_mask(cidr):
return socket.inet_ntop(
socket.AF_INET, struct.pack('!I', (2**32 - 1) ^ (2**(32 - cidr) - 1)))
def ip_on_same_subnet(first, second, prefix):
addrinf = socket.getaddrinfo(first, None, 0, socket.SOCK_STREAM)[0]
fam = addrinf[0]
+51 -15
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016, 2017 Lenovo
# Copyright 2016-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ import eventlet
from eventlet.greenpool import GreenPool
import eventlet.semaphore
import re
webclient = eventlet.import_patched('pyghmi.util.webclient')
# The interesting OIDs are:
# lldpLocChassisId - to cross reference (1.0.8802.1.1.2.1.3.2.0)
# lldpLocPortId - for cross referencing.. (1.0.8802.1.1.2.1.3.7.1.3)
@@ -85,6 +85,7 @@ _neighdata = {}
_neighbypeerid = {}
_updatelocks = {}
_chassisidbyswitch = {}
_noaffluent = set([])
def lenovoname(idx, desc):
if desc.isdigit():
@@ -105,7 +106,11 @@ def close_enough(fuzz, literal):
if fuzz == literal:
return True
fuzz = '^' + fuzz.replace('-', '[/: -]') + '$'
matcher = re.compile(fuzz)
try:
matcher = re.compile(fuzz)
except Exception:
raise exc.InvalidArgumentException(
'Invalid regular expression specified')
return bool(matcher.match(literal))
@@ -121,7 +126,8 @@ def _dump_neighbordatum(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_fingerprint(switch, port, configmanager, portmatch):
update_switch_data(switch, configmanager)
@@ -166,21 +172,58 @@ def _init_lldp(data, iname, idx, idxtoportid, switch):
data[iname] = {'port': iname, 'portid': str(idxtoportid[idx]),
'chassisid': _chassisidbyswitch[switch]}
def _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata):
kv = util.TLSCertVerifier(cfm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
wc = webclient.SecureHTTPConnection(
switch, 443, verifycallback=kv, timeout=5)
wc.set_basic_credentials(user, password)
neighdata = wc.grab_json_response('/affluent/lldp/all')
chassisid = neighdata['chassis']['id']
_chassisidbyswitch[switch] = chassisid,
for record in neighdata['neighbors']:
localport = record['localport']
peerid = '{0}.{1}'.format(
record.get('peerchassisid', '').replace(':', '-').replace('/', '-'),
record.get('peerportid', '').replace(':', '-').replace('/', '-'),
)
portdata = {
'verified': True, # It is over TLS after all
'peerdescription': record.get('peerdescription', None),
'peerchassisid': record['peerchassisid'],
'peername': record['peername'],
'switch': switch,
'chassisid': chassisid,
'portid': record['localport'],
'peerportid': record['peerportid'],
'port': record['localport'],
'peerid': peerid,
}
_neighbypeerid[peerid] = portdata
lldpdata[localport] = portdata
neighdata[switch] = lldpdata
def _extract_neighbor_data_b(args):
"""Build LLDP data about elements connected to switch
args are carried as a tuple, because of eventlet convenience
"""
switch, password, user, force = args[:4]
switch, password, user, cfm, force = args[:5]
vintage = _neighdata.get(switch, {}).get('!!vintage', 0)
now = util.monotonic_time()
if vintage > (now - 60) and not force:
return
lldpdata = {'!!vintage': now}
try:
return _extract_neighbor_data_affluent(switch, user, password, cfm, lldpdata)
except Exception:
pass
conn = snmp.Session(switch, password, user)
sid = None
lldpdata = {'!!vintage': now}
for sysid in conn.walk('1.3.6.1.2.1.1.2'):
sid = str(sysid[1][6:])
_noaffluent.add(switch)
idxtoifname = {}
idxtoportid = {}
_chassisidbyswitch[switch] = sanitize(list(
@@ -263,8 +306,8 @@ def _extract_neighbor_data(args):
return _extract_neighbor_data_b(args)
except Exception as e:
yieldexc = False
if len(args) >= 5:
yieldexc = args[4]
if len(args) >= 6:
yieldexc = args[5]
if yieldexc:
return e
else:
@@ -353,10 +396,3 @@ def _handle_neighbor_query(pathcomponents, configmanager):
raise x
return list_info(parms, listrequested)
def _list_interfaces(switchname, configmanager):
switchcreds = get_switchcreds(configmanager, (switchname,))
switchcreds = switchcreds[0]
conn = snmp.Session(*switchcreds)
ifnames = netutil.get_portnamemap(conn)
return util.natural_sort(ifnames.values())
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
# Copyright 2016-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -45,13 +45,16 @@ from eventlet.greenpool import GreenPool
import eventlet
import eventlet.semaphore
import re
webclient = eventlet.import_patched('pyghmi.util.webclient')
noaffluent = set([])
_macmap = {}
_apimacmap = {}
_macsbyswitch = {}
_nodesbymac = {}
_switchportmap = {}
_neighdata = {}
vintage = None
@@ -127,6 +130,36 @@ def _nodelookup(switch, ifname):
return None
def _affluent_map_switch(args):
switch, password, user, cfm = args
kv = util.TLSCertVerifier(cfm, switch,
'pubkeys.tls_hardwaremanager').verify_cert
wc = webclient.SecureHTTPConnection(
switch, 443, verifycallback=kv, timeout=5)
wc.set_basic_credentials(user, password)
macs = wc.grab_json_response('/affluent/macs/by-port')
_macsbyswitch[switch] = macs
for iface in macs:
nummacs = len(macs[iface])
for mac in macs[iface]:
if mac in _macmap:
_macmap[mac].append((switch, iface, nummacs))
else:
_macmap[mac] = [(switch, iface, nummacs)]
nodename = _nodelookup(switch, iface)
if nodename is not None:
if mac in _nodesbymac and _nodesbymac[mac][0] != nodename:
# For example, listed on both a real edge port
# and by accident a trunk port
log.log({'error': '{0} and {1} described by ambiguous'
' switch topology values'.format(
nodename, _nodesbymac[mac][0])})
_nodesbymac[mac] = (None, None)
else:
_nodesbymac[mac] = (nodename, nummacs)
def _map_switch_backend(args):
"""Manipulate portions of mac address map relevant to a given switch
"""
@@ -144,13 +177,18 @@ def _map_switch_backend(args):
# fallback if ifName is empty
#
global _macmap
if len(args) == 3:
switch, password, user = args
if len(args) == 4:
switch, password, user, cfm = args
if not user:
user = None
else:
switch, password = args
user = None
if switch not in noaffluent:
try:
return _affluent_map_switch(args)
except Exception:
pass
haveqbridge = False
mactobridge = {}
conn = snmp.Session(switch, password, user)
@@ -164,6 +202,7 @@ def _map_switch_backend(args):
*([int(x) for x in oid[-6:]])
)
mactobridge[macaddr] = int(bridgeport)
noaffluent.add(switch)
if not haveqbridge:
for vb in conn.walk('1.3.6.1.2.1.17.4.3.1.2'):
oid, bridgeport = vb
@@ -380,7 +419,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if (operation in ('update', 'create') and
pathcomponents == ['networking', 'macs', 'rescan']):
if inputdata != {'rescan': 'start'}:
raise exc.InvalidArgumentException()
raise exc.InvalidArgumentException('Input must be rescan=start')
eventlet.spawn_n(rescan, configmanager)
return [msg.KeyValueData({'rescan': 'started'})]
raise exc.NotImplementedException(
@@ -458,9 +497,21 @@ def handle_read_api_request(pathcomponents, configmanager):
portname = portname.replace('-', '/')
maclist = _macsbyswitch[switchname][portname]
except KeyError:
raise exc.NotFoundException('No known macs for switch {0} '
'port {1}'.format(switchname,
portname))
foundsomemacs = False
if switchname in _macsbyswitch:
try:
matcher = re.compile(portname)
except Exception:
raise exc.InvalidArgumentException('Invalid regular expression specified')
maclist = []
for actualport in _macsbyswitch[switchname]:
if bool(matcher.match(actualport)):
foundsomemacs = True
maclist = maclist + _macsbyswitch[switchname][actualport]
if not foundsomemacs:
raise exc.NotFoundException('No known macs for switch {0} '
'port {1}'.format(switchname,
portname))
return [msg.ChildCollection(x.replace(':', '-'))
for x in sorted(maclist)]
if len(pathcomponents) == 8:
@@ -36,7 +36,7 @@ def get_switchcreds(configmanager, switches):
'secret.hardwaremanagementuser', {}).get('value', None)
if not user:
user = None
switchauth.append((switch, password, user))
switchauth.append((switch, password, user, configmanager))
return switchauth
+12 -5
View File
@@ -25,6 +25,11 @@ import itertools
import pyparsing as pp
import re
try:
range = xrange
except NameError:
pass
# construct custom grammar with pyparsing
_nodeword = pp.Word(pp.alphanums + '~^$/=-_:.*+!')
_nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']',
@@ -157,7 +162,7 @@ class NodeRange(object):
pieces = seqrange.split(delimiter)
if len(pieces) % 2 != 0:
return self.failorreturn(seqrange)
halflen = len(pieces) / 2
halflen = len(pieces) // 2
left = delimiter.join(pieces[:halflen])
right = delimiter.join(pieces[halflen:])
leftbits = _numextractor.parseString(left).asList()
@@ -166,7 +171,7 @@ class NodeRange(object):
return self.failorreturn(seqrange)
finalfmt = ''
iterators = []
for idx in xrange(len(leftbits)):
for idx in range(len(leftbits)):
if leftbits[idx] == rightbits[idx]:
finalfmt += leftbits[idx]
elif leftbits[idx][0] in pp.alphas:
@@ -181,7 +186,7 @@ class NodeRange(object):
if leftnum > rightnum:
width = len(rightbits[idx])
minnum = rightnum
maxnum = leftnum + 1 # xrange goes to n-1...
maxnum = leftnum + 1 # range goes to n-1...
elif rightnum > leftnum:
width = len(leftbits[idx])
minnum = leftnum
@@ -189,7 +194,7 @@ class NodeRange(object):
else: # differently padded, but same number...
return self.failorreturn(seqrange)
numformat = '{0:0%d}' % width
for num in xrange(minnum, maxnum):
for num in range(minnum, maxnum):
curseq.append(numformat.format(num))
results = set([])
for combo in itertools.product(*iterators):
@@ -222,7 +227,7 @@ class NodeRange(object):
if self.cfm is None:
raise Exception('Verification configmanager required')
return set(self.cfm.filter_node_attributes(element, filternodes))
for idx in xrange(len(element)):
for idx in range(len(element)):
if element[idx][0] == '[':
nodes = set([])
for numeric in NodeRange(element[idx][1:-1]).nodes:
@@ -245,6 +250,8 @@ class NodeRange(object):
return nodes
if ':' in element: # : range for less ambiguity
return self.expandrange(element, ':')
elif '..' in element:
return self.expandrange(element, '..')
elif '-' in element:
return self.expandrange(element, '-')
elif '+' in element:
+235
View File
@@ -0,0 +1,235 @@
# Pulled from:
# https://raw.githubusercontent.com/FirefighterBlu3/python-pam/fe44b334970f421635d9e373b563c9e6566613bd/pam.py
# and https://github.com/FirefighterBlu3/python-pam/pull/16/files
# (c) 2007 Chris AtLee <chris@atlee.ca>
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
#
# Original author: Chris AtLee
#
# Modified by David Ford, 2011-12-6
# added py3 support and encoding
# added pam_end
# added pam_setcred to reset credentials after seeing Leon Walker's remarks
# added byref as well
# use readline to prestuff the getuser input
'''
PAM module for python
Provides an authenticate function that will allow the caller to authenticate
a user against the Pluggable Authentication Modules (PAM) on the system.
Implemented using ctypes, so no compilation is necessary.
'''
__all__ = ['pam']
__version__ = '1.8.4'
__author__ = 'David Ford <david@blue-labs.org>'
__released__ = '2018 June 15'
import sys
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
from ctypes import memmove
from ctypes.util import find_library
class PamHandle(Structure):
"""wrapper class for pam_handle_t pointer"""
_fields_ = [ ("handle", c_void_p) ]
def __init__(self):
Structure.__init__(self)
self.handle = 0
class PamMessage(Structure):
"""wrapper class for pam_message structure"""
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]
def __repr__(self):
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
class PamResponse(Structure):
"""wrapper class for pam_response structure"""
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]
def __repr__(self):
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)
class PamConv(Structure):
"""wrapper class for pam_conv structure"""
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]
# Various constants
PAM_PROMPT_ECHO_OFF = 1
PAM_PROMPT_ECHO_ON = 2
PAM_ERROR_MSG = 3
PAM_TEXT_INFO = 4
PAM_REINITIALIZE_CRED = 8
libc = CDLL(find_library("c"))
libpam = CDLL(find_library("pam"))
calloc = libc.calloc
calloc.restype = c_void_p
calloc.argtypes = [c_size_t, c_size_t]
# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
if hasattr(libpam, 'pam_end'):
pam_end = libpam.pam_end
pam_end.restype = c_int
pam_end.argtypes = [PamHandle, c_int]
pam_start = libpam.pam_start
pam_start.restype = c_int
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
pam_acct_mgmt = libpam.pam_acct_mgmt
pam_acct_mgmt.restype = c_int
pam_acct_mgmt.argtypes = [PamHandle, c_int]
pam_setcred = libpam.pam_setcred
pam_setcred.restype = c_int
pam_setcred.argtypes = [PamHandle, c_int]
pam_strerror = libpam.pam_strerror
pam_strerror.restype = c_char_p
pam_strerror.argtypes = [PamHandle, c_int]
pam_authenticate = libpam.pam_authenticate
pam_authenticate.restype = c_int
pam_authenticate.argtypes = [PamHandle, c_int]
class pam():
code = 0
reason = None
def __init__(self):
pass
def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True):
"""username and password authentication for the given service.
Returns True for success, or False for failure.
self.code (integer) and self.reason (string) are always stored and may
be referenced for the reason why authentication failed. 0/'Success' will
be stored for success.
Python3 expects bytes() for ctypes inputs. This function will make
necessary conversions using the supplied encoding.
Inputs:
username: username to authenticate
password: password in plain text
service: PAM service to authenticate against, defaults to 'login'
Returns:
success: True
failure: False
"""
@conv_func
def my_conv(n_messages, messages, p_response, app_data):
"""Simple conversation function that responds to any
prompt where the echo is off with the supplied password"""
# Create an array of n_messages response objects
addr = calloc(n_messages, sizeof(PamResponse))
response = cast(addr, POINTER(PamResponse))
p_response[0] = response
for i in range(n_messages):
if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
dst = calloc(len(password)+1, sizeof(c_char))
memmove(dst, cpassword, len(password))
response[i].resp = dst
response[i].resp_retcode = 0
return 0
# python3 ctypes prefers bytes
if sys.version_info >= (3,):
if isinstance(username, str): username = username.encode(encoding)
if isinstance(password, str): password = password.encode(encoding)
if isinstance(service, str): service = service.encode(encoding)
else:
if isinstance(username, unicode):
username = username.encode(encoding)
if isinstance(password, unicode):
password = password.encode(encoding)
if isinstance(service, unicode):
service = service.encode(encoding)
if b'\x00' in username or b'\x00' in password or b'\x00' in service:
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
self.reason = 'strings may not contain NUL'
return False
# do this up front so we can safely throw an exception if there's
# anything wrong with it
cpassword = c_char_p(password)
handle = PamHandle()
conv = PamConv(my_conv, 0)
retval = pam_start(service, username, byref(conv), byref(handle))
if retval != 0:
# This is not an authentication error, something has gone wrong starting up PAM
self.code = retval
self.reason = "pam_start() failed"
return False
retval = pam_authenticate(handle, 0)
auth_success = retval == 0
if auth_success:
retval = pam_acct_mgmt(handle, 0)
auth_success = retval == 0
if auth_success and resetcreds:
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED)
# store information to inform the caller why we failed
self.code = retval
self.reason = pam_strerror(handle, retval)
if sys.version_info >= (3,):
self.reason = self.reason.decode(encoding)
if hasattr(libpam, 'pam_end'):
pam_end(handle, retval)
return auth_success
def authenticate(*vargs, **dargs):
"""
Compatibility function for older versions of python-pam.
"""
return pam().authenticate(*vargs, **dargs)
if __name__ == "__main__":
import readline, getpass
def input_with_prefill(prompt, text):
def hook():
readline.insert_text(text)
readline.redisplay()
readline.set_pre_input_hook(hook)
if sys.version_info >= (3,):
result = input(prompt)
else:
result = raw_input(prompt)
readline.set_pre_input_hook()
return result
pam = pam()
username = input_with_prefill('Username: ', getpass.getuser())
# enter a valid username and an invalid/valid password, to verify both failure and success
pam.authenticate(username, getpass.getpass())
print('{} {}'.format(pam.code, pam.reason))
@@ -17,6 +17,7 @@ import confluent.exceptions as exc
import confluent.messages as msg
import confluent.config.attributes as allattributes
import confluent.util as util
from fnmatch import fnmatch
def retrieve(nodes, element, configmanager, inputdata):
@@ -133,7 +134,7 @@ def retrieve_nodes(nodes, element, configmanager, inputdata):
attribute, {}).get('description', ''))
elif element[-1] == 'current':
for node in util.natural_sort(list(attributes)):
for attribute in sorted(attributes[node].iterkeys()):
for attribute in sorted(attributes[node]):
currattr = attributes[node][attribute]
try:
desc = allattributes.node[attribute]['description']
@@ -148,8 +149,8 @@ def retrieve_nodes(nodes, element, configmanager, inputdata):
yield msg.ListAttributes(
node, {attribute: currattr}, desc)
else:
print attribute
print repr(currattr)
print(attribute)
print(repr(currattr))
raise Exception("BUGGY ATTRIBUTE FOR NODE")
@@ -163,6 +164,20 @@ def update(nodes, element, configmanager, inputdata):
def update_nodegroup(group, element, configmanager, inputdata):
if element == 'check':
check = inputdata.attribs
decrypt = configmanager.decrypt
configmanager.decrypt = True
currinfo = configmanager.get_nodegroup_attributes(group, list(check))
configmanager.decrypt = decrypt
for inf in check:
checkvalue = check[inf]
if isinstance(checkvalue, dict):
checkvalue = checkvalue.get('value', None)
currvalue = currinfo.get(inf, {}).get('value')
if checkvalue == currvalue:
raise exc.InvalidArgumentException('Checked value matches existing value')
return retrieve_nodegroup(group, element, configmanager, inputdata)
if 'rename' in element:
namemap = {}
namemap[group] = inputdata.attribs['rename']
@@ -170,7 +185,7 @@ def update_nodegroup(group, element, configmanager, inputdata):
return yield_rename_resources(namemap, isnode=False)
try:
clearattribs = []
for attrib in inputdata.attribs.iterkeys():
for attrib in inputdata.attribs:
if inputdata.attribs[attrib] is None:
clearattribs.append(attrib)
for attrib in clearattribs:
@@ -220,6 +235,18 @@ def update_nodes(nodes, element, configmanager, inputdata):
raise exc.InvalidArgumentException(
'No action to take, noderange is empty (if trying to define '
'group attributes, use nodegroupattrib)')
if element[-1] == 'check':
for node in nodes:
check = inputdata.get_attributes(node, allattributes.node)
currinfo = configmanager.get_node_attributes(node, list(check), decrypt=True)
for inf in check:
checkvalue = check[inf]
if isinstance(checkvalue, dict):
checkvalue = checkvalue.get('value', None)
currvalue = currinfo.get(node, {}).get(inf, {}).get('value')
if checkvalue == currvalue:
raise exc.InvalidArgumentException('Checked value matches existing value')
return retrieve(nodes, element, configmanager, inputdata)
if 'rename' in element:
namemap = {}
for node in nodes:
@@ -231,12 +258,20 @@ def update_nodes(nodes, element, configmanager, inputdata):
updatenode = inputdata.get_attributes(node, allattributes.node)
clearattribs = []
if updatenode:
for attrib in updatenode.iterkeys():
for attrib in list(updatenode):
if updatenode[attrib] is None:
clearattribs.append(attrib)
if len(clearattribs) > 0:
for attrib in clearattribs:
del updatenode[attrib]
if attrib in allattributes.node or attrib.startswith('custom.') or attrib.startswith('net.'):
clearattribs.append(attrib)
else:
foundattrib = False
for candattrib in allattributes.node:
if fnmatch(candattrib, attrib):
clearattribs.append(candattrib)
foundattrib = True
if not foundattrib:
raise exc.InvalidArgumentException("No attribute matches '" + attrib + "' (try wildcard if trying to clear a group)")
if len(clearattribs) > 0:
configmanager.clear_node_attributes([node], clearattribs)
updatedict[node] = updatenode
try:
@@ -0,0 +1,160 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This plugin provides an ssh implementation comforming to the 'console'
# specification. consoleserver or shellserver would be equally likely
# to use this.
import confluent.exceptions as cexc
import confluent.interface.console as conapi
import confluent.log as log
import confluent.util as util
import pyghmi.exceptions as pygexc
import pyghmi.redfish.command as rcmd
import eventlet
import eventlet.green.ssl as ssl
try:
websocket = eventlet.import_patched('websocket')
wso = websocket.WebSocket
except Exception:
wso = object
def get_conn_params(node, configdata):
if 'secret.hardwaremanagementuser' in configdata:
username = configdata['secret.hardwaremanagementuser']['value']
else:
username = 'USERID'
if 'secret.hardwaremanagementpassword' in configdata:
passphrase = configdata['secret.hardwaremanagementpassword']['value']
else:
passphrase = 'PASSW0RD' # for lack of a better guess
if 'hardwaremanagement.manager' in configdata:
bmc = configdata['hardwaremanagement.manager']['value']
else:
bmc = node
return {
'username': username,
'passphrase': passphrase,
'bmc': bmc,
}
_configattributes = ('secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager')
class WrappedWebSocket(wso):
def set_verify_callback(self, callback):
self._certverify = callback
def connect(self, url, **options):
add_tls = url.startswith('wss://')
if add_tls:
hostname, port, resource, _ = websocket._url.parse_url(url)
if hostname[0] != '[' and ':' in hostname:
hostname = '[{0}]'.format(hostname)
if resource[0] != '/':
resource = '/{0}'.format(resource)
url = 'ws://{0}:443{1}'.format(hostname,resource)
else:
return super(WrappedWebSocket, self).connect(url, **options)
self.sock_opt.timeout = options.get('timeout', self.sock_opt.timeout)
self.sock, addrs = websocket._http.connect(url, self.sock_opt, websocket._http.proxy_info(**options),
options.pop('socket', None))
self.sock = ssl.wrap_socket(self.sock, cert_reqs=ssl.CERT_NONE)
# The above is supersedeed by the _certverify, which provides
# known-hosts style cert validaiton
bincert = self.sock.getpeercert(binary_form=True)
if not self._certverify(bincert):
raise pygexc.UnrecognizedCertificate('Unknown certificate', bincert)
try:
self.handshake_response = websocket._handshake.handshake(self.sock, *addrs, **options)
if self.handshake_response.status in websocket._handshake.SUPPORTED_REDIRECT_STATUSES:
options['redirect_limit'] = options.pop('redirect_limit', 3) - 1
if options['redirect_limit'] < 0:
raise Exception('Redirect limit hit')
url = self.handshake_response.headers['location']
self.sock.close()
return self.connect(url, **options)
self.connected = True
except:
if self.sock:
self.sock.close()
self.sock = None
raise
class TsmConsole(conapi.Console):
def __init__(self, node, config):
self.node = node
self.ws = None
configdata = config.get_node_attributes([node], _configattributes, decrypt=True)
connparams = get_conn_params(node, configdata[node])
self.username = connparams['username']
self.password = connparams['passphrase']
self.bmc = connparams['bmc']
self.origbmc = connparams['bmc']
if ':' in self.bmc:
self.bmc = '[{0}]'.format(self.bmc)
self.datacallback = None
self.nodeconfig = config
self.connected = False
def recvdata(self):
while self.connected:
pendingdata = self.ws.recv()
if pendingdata == '':
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
self.datacallback(pendingdata)
def connect(self, callback):
self.datacallback = callback
rc = rcmd.Command(self.origbmc, self.username,
self.password,
verifycallback=lambda x: True)
wc = rc.oem.wc
bmc = self.bmc
if '%' in self.bmc:
prefix = self.bmc.split('%')[0]
bmc = prefix + ']'
self.ws = WrappedWebSocket(host=bmc)
kv = util.TLSCertVerifier(
self.nodeconfig, self.node, 'pubkeys.tls_hardwaremanager').verify_cert
self.ws.set_verify_callback(kv)
self.ws.connect('wss://{0}/sol?CSRFTOKEN={1}'.format(self.bmc, rc.oem.csrftok), host=bmc, cookie='QSESSIONID={0}'.format(wc.cookies['QSESSIONID']))
self.connected = True
eventlet.spawn_n(self.recvdata)
return
def write(self, data):
self.ws.send(data)
def close(self):
if self.ws:
self.ws.close()
self.connected = False
self.datacallback = None
def create(nodes, element, configmanager, inputdata):
if len(nodes) == 1:
return TsmConsole(nodes[0], configmanager)
@@ -0,0 +1,153 @@
# Copyright 2019-2020 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 eventlet
import eventlet.queue as queue
import confluent.exceptions as exc
webclient = eventlet.import_patched('pyghmi.util.webclient')
import confluent.messages as msg
import confluent.util as util
class SwitchSensor(object):
def __init__(self, name, states, value=None, health=None):
self.name = name
self.value = value
self.states = states
self.health = health
class WebClient(object):
def __init__(self, node, configmanager, creds):
self.node = node
self.wc = webclient.SecureHTTPConnection(node, port=443, verifycallback=util.TLSCertVerifier(
configmanager, node, 'pubkeys.tls_hardwaremanager').verify_cert)
self.wc.set_basic_credentials(creds[node]['secret.hardwaremanagementuser']['value'], creds[node]['secret.hardwaremanagementpassword']['value'])
def fetch(self, url, results):
rsp, status = self.wc.grab_json_response_with_status(url)
if status == 401:
results.put(msg.ConfluentTargetInvalidCredentials(self.node, 'Unable to authenticate'))
return {}
elif status != 200:
results.put(msg.ConfluentNodeError(self.node, 'Unknown error: ' + rsp + ' while retrieving ' + url))
return {}
return rsp
def update(nodes, element, configmanager, inputdata):
for node in nodes:
yield msg.ConfluentNodeError(node, 'Not Implemented')
def delete(nodes, element, configmanager, inputdata):
for node in nodes:
yield msg.ConfluentNodeError(node, 'Not Implemented')
def create(nodes, element, configmanager, inputdata):
for node in nodes:
yield msg.ConfluentNodeError(node, 'Not Implemented')
def _run_method(method, workers, results, configmanager, nodes, element):
creds = configmanager.get_node_attributes(
nodes, ['secret.hardwaremanagementuser', 'secret.hardwaremanagementpassword'], decrypt=True)
for node in nodes:
workers.add(eventlet.spawn(method, configmanager, creds,
node, results, element))
def retrieve(nodes, element, configmanager, inputdata):
results = queue.LightQueue()
workers = set([])
if element == ['power', 'state']:
for node in nodes:
yield msg.PowerState(node=node, state='on')
return
elif element == ['health', 'hardware']:
_run_method(retrieve_health, workers, results, configmanager, nodes, element)
elif element[:3] == ['inventory', 'hardware', 'all']:
_run_method(retrieve_inventory, workers, results, configmanager, nodes, element)
elif element[:3] == ['inventory', 'firmware', 'all']:
_run_method(retrieve_firmware, workers, results, configmanager, nodes, element)
elif element == ['sensors', 'hardware', 'all']:
_run_method(list_sensors, workers, results, configmanager, nodes, element)
elif element[:3] == ['sensors', 'hardware', 'all']:
_run_method(retrieve_sensors, workers, results, configmanager, nodes, element)
else:
for node in nodes:
yield msg.ConfluentNodeError(node, 'Not Implemented')
return
while workers:
try:
datum = results.get(10)
while datum:
if datum:
yield datum
datum = results.get_nowait()
except queue.Empty:
pass
eventlet.sleep(0.001)
for t in list(workers):
if t.dead:
workers.discard(t)
try:
while True:
datum = results.get_nowait()
if datum:
yield datum
except queue.Empty:
pass
def retrieve_inventory(configmanager, creds, node, results, element):
if len(element) == 3:
results.put(msg.ChildCollection('all'))
results.put(msg.ChildCollection('system'))
return
wc = WebClient(node, configmanager, creds)
invinfo = wc.fetch('/affluent/inventory/hardware/all', results)
if invinfo:
results.put(msg.KeyValueData(invinfo, node))
def retrieve_firmware(configmanager, creds, node, results, element):
if len(element) == 3:
results.put(msg.ChildCollection('all'))
return
wc = WebClient(node, configmanager, creds)
fwinfo = wc.fetch('/affluent/inventory/firmware/all', results)
if fwinfo:
results.put(msg.Firmware(fwinfo, node))
def list_sensors(configmanager, creds, node, results, element):
wc = WebClient(node, configmanager, creds)
sensors = wc.fetch('/affluent/sensors/hardware/all', results)
for sensor in sensors['item']:
results.put(msg.ChildCollection(sensor))
def retrieve_sensors(configmanager, creds, node, results, element):
wc = WebClient(node, configmanager, creds)
sensors = wc.fetch('/affluent/sensors/hardware/all/{0}'.format(element[-1]), results)
if sensors:
results.put(msg.SensorReadings(sensors['sensors'], node))
def retrieve_health(configmanager, creds, node, results, element):
wc = WebClient(node, configmanager, creds)
hinfo = wc.fetch('/affluent/health', results)
if hinfo:
results.put(msg.HealthSummary(hinfo.get('health', 'unknown'), name=node))
results.put(msg.SensorReadings(hinfo.get('sensors', []), name=node))

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