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

Compare commits

...

165 Commits

Author SHA1 Message Date
Jarrod Johnson ba9d62b4e5 Merge pull request #53 from jjohnson42/fixnonrootconfetty
No longer break when prompting for name/password
2015-11-16 14:56:44 -05:00
Jarrod Johnson 4442ce1c71 No longer break when prompting for name/password
In the confetty CLI, readline clear history was done as
part of login process.  Since readline is not a given
to accomodate scripting behaviors, no longer do the
clear_history().  The concern I had was that
the password might have gotten into history, but that
seems to not be the case.
2015-11-10 12:56:04 -05:00
Jarrod Johnson 1d64792cb9 Merge pull request #52 from jjohnson42/nopam
Remove hard require of PyPAM
2015-11-10 11:11:32 -05:00
Jarrod Johnson e721f8836e Remove hard require of PyPAM
Since PAM support is not a mandatory feature,
do not make it required to install.
2015-11-10 11:10:11 -05:00
Jarrod Johnson 6770fe9342 Merge pull request #51 from jjohnson42/fixdynamicgroups
Correct dynamic group behavior
2015-11-06 11:57:05 -05:00
Jarrod Johnson 804d4c2d95 Correct dynamic group behavior
Expansion of a noderange containing a dynamic group
would erroneously make the dynamic members get promoted
to 'permament' members.  Fix this by isolating the
change so that the underlying group config is not changed
just by adding in dynamic nodes.
2015-11-06 11:55:13 -05:00
Jarrod Johnson f946fab3c5 Merge pull request #50 from jjohnson42/fixhttpapi
Force 'databynode' to consistently be an array
2015-11-03 09:10:35 -05:00
Jarrod Johnson 5a0ac899b9 Force 'databynode' to consistently be an array
If only one node was in given noderange, then
the httpapi response would not look similar in
structure to a multi-node response.  Force even
single items in this special case to be an array
to allow easier javascript client code.
2015-11-03 09:08:18 -05:00
Jarrod Johnson 310bd11669 Merge pull request #49 from jjohnson42/fixsystemd
Prevent hang on systemctl stop confluent
2015-11-02 14:13:23 -05:00
Jarrod Johnson 5ab1d6ea59 Prevent hang on systemctl stop confluent
systemd's default stop seems to be incapable of understanding
how to shut down our service.  Provide an explicit ExecStop
to have systemd act more sanely.
2015-11-02 14:11:07 -05:00
Jarrod Johnson d8c3b2f267 Merge pull request #48 from jjohnson42/debugsignals
Add facility to dump trace to log
2015-10-28 10:46:09 -04:00
Jarrod Johnson f02c74cce0 Add facility to dump trace to log
If confluent gets stuck, provide a debug facility
to sample where it is stuck.  Sending confluent
SIGUSR1 will now cause /var/log/confluent/hangtraces
to get written to.
2015-10-28 10:45:18 -04:00
Jarrod Johnson 5485ef36bf Merge pull request #47 from jjohnson42/amendconsoleapi
Amend console API
2015-10-21 16:46:32 -04:00
Jarrod Johnson 1cf238708f Amend console API
The console API had a 'wait_for_data' which will never be used.
Also change __init__() so that it can be called via super() to
allow working around IDE complaints.
2015-10-21 16:44:09 -04:00
Jarrod Johnson 80aa2c477a Merge pull request #46 from jjohnson42/handleunicodestringoutput
Handle unicode string from a console plugin
2015-10-21 13:26:32 -04:00
Jarrod Johnson 9868e55958 Handle unicode string from a console plugin
If a console plugin feels like outputting data
in a unicode string, accept that data by encoding
to utf-8.
2015-10-21 13:24:41 -04:00
Jarrod Johnson 7a8fbe7d62 Merge pull request #45 from jjohnson42/fixnullclient
Fixnullclient
2015-10-21 10:33:50 -04:00
Jarrod Johnson 52aaeef506 Have server avoid sending empty data
While the client can handle it now, have the server
avoid needless processing of '' data from a console
provider.  Address it at the deepest level (the
tlvdata implementation) and a place higher up the stack
to avoid hits to log and such.
2015-10-21 10:30:41 -04:00
Jarrod Johnson be8d82c6c1 Client behavior fix when server sends 0 data
If the server sends zero data, client could hang as it does recv(0).  Fix this by
returning None in that scenario.
2015-10-21 10:22:49 -04:00
Jarrod Johnson a83583a56d Merge pull request #44 from jjohnson42/master
Merge fixes in prep for 1.2.0 tag
2015-10-19 15:25:11 -04:00
Jarrod Johnson f906cebca3 Merge remote-tracking branch 'upstream/master' 2015-10-19 15:23:19 -04:00
Jarrod Johnson e153e8acd0 Correct indentation in nodeeventlog
Mistakes were present in the whitespace for nodeeventlog, apply correct indentation.
2015-10-19 14:27:11 -04:00
Jarrod Johnson dec0543ce3 Fix nodelist formatting of error
Error needed a newline after message to be clear.
2015-10-19 14:23:02 -04:00
Jarrod Johnson 204f6de7e9 Fix missing 'nodeeventlog' command in packaging
When packaging, nodeeventlog was missed. Fix the setup.py.tmpl
to include the missing script.
2015-10-19 14:14:54 -04:00
Jarrod Johnson 76906c191b Replace '/' in '-' in ipmi names
'/' confuses our '/' delimited namespace.  Use '-'
instead.  '_or_' was considered, but other meanings
could be applied other than '/'.
2015-10-19 14:13:02 -04:00
Jarrod Johnson 61a0a66486 Fix nodelist handling of bad noderange
When attributes were specified with an incorrect noderange
it was failing to report the noderange error.  If that were
specifically addressed, it would then proceed to erroneously
complain about valid attribute names.
2015-10-19 14:09:37 -04:00
Jarrod Johnson 597bc9413f Merge pull request #43 from jjohnson42/master
Change to versioning derivation from git data
2015-10-19 14:07:21 -04:00
Jarrod Johnson e53a9f83f4 Change to versioning derivation from git data
Rather than manually curate the VERSION, use git tags
and auto-create intermediate builds with unique identifiers.
Identify both number of commits to indicate order and the git
short rev to see which rev matches.
2015-10-19 14:06:59 -04:00
Jarrod Johnson 31a191cb10 Merge pull request #42 from jjohnson42/fixpidfile
Fix pid file behavior on exit
2015-10-15 09:44:35 -04:00
Jarrod Johnson 8d566c1795 Merge pull request #40 from jufm/timeout
Add support for event ack timeout setting.
2015-10-15 09:44:13 -04:00
Jarrod Johnson 2c2884f80f Fix pid file behavior on exit
Previous change failed to correct the sense of the 'doexit'
function.
2015-10-15 09:39:44 -04:00
Allan Vidal fe4797857d Add support for event ack timeout setting. 2015-10-15 09:41:13 -03:00
Jarrod Johnson e90204cc4d Merge pull request #41 from jjohnson42/fixpidfile
Fix pid file creation
2015-10-14 15:52:46 -04:00
Jarrod Johnson d4828b2115 Fix pid file creation
The windows support to 'gracefully' deal with no fcntl
incorrectly broke fcntl usage under linux in main.  Fix
the check to be accurate.
2015-10-14 15:30:43 -04:00
Jarrod Johnson 7b26d2edfb Merge pull request #39 from jjohnson42/fixnodelistnoarg
Fix bad or missing noderange behavior in nodelist
2015-10-14 11:10:34 -04:00
Jarrod Johnson 0bb0368d07 Fix bad or missing noderange behavior in nodelist
nodelist command would show 'help' with no arguments, but
natural expectation is to list all nodes.  Adjust to match
that expectation.  If a noderange was somehow problematic,
the output was not appropriate, this too is addressed.
2015-10-14 11:07:31 -04:00
Jarrod Johnson 1467e1f172 Merge pull request #38 from jufm/rundir
Fix unix socket dir creation
2015-10-14 11:05:21 -04:00
Juliana Motira 06f22e7acb Fix unix socket dir creation 2015-10-14 09:53:14 -03:00
Jarrod Johnson 8779f1c27f Merge pull request #37 from jufm/stringfix
Fix encoded string problem in InputNTPServer
2015-10-13 14:56:55 -04:00
Juliana Motira 2245e53ff1 Fix encoded string problem in InputNTPServer 2015-10-13 15:51:37 -03:00
Jarrod Johnson 99d1edcdef Merge pull request #36 from jjohnson42/versionbump
Bump version to 1.2
2015-10-13 14:16:44 -04:00
Jarrod Johnson fbca02e262 Bump version to 1.2 2015-10-13 14:16:02 -04:00
Jarrod Johnson d533321d55 Merge pull request #35 from jjohnson42/addcommands
Add nodeconsole and nodelist commands
2015-10-13 14:14:59 -04:00
Jarrod Johnson 0b7b713f3f Add nodeconsole and nodelist commands
nodeconsole provides a wrapper for confetty offering logical
tab completion for console specifically as well as help
text to explain critical info about using nodeconsole.
nodelist provides functionality analagous to nodels in
xCAT 2.x codebase
2015-10-13 14:12:43 -04:00
Victor Hu 865545b8d1 Merge pull request #34 from jjohnson42/confluentpath
Have confluent client do default path
2015-10-12 21:37:45 -04:00
Jarrod Johnson 258e257939 Have confluent client do default path
When installing package, put a profile.d change
such that confluent is in the default path.
2015-10-12 17:00:40 -04:00
Jarrod Johnson 127fc59195 Merge pull request #33 from jjohnson42/master
Fix valid noderange character
2015-10-09 15:01:36 -04:00
Jarrod Johnson 8c4d33db92 Merge remote-tracking branch 'upstream/master' 2015-10-09 15:00:35 -04:00
Jarrod Johnson b1240e327f Fix validity of underscore in noderange
A group name may use underscore in name per xCAT syntax.  Fix
grammar to accept _
2015-10-09 14:53:42 -04:00
Jarrod Johnson 477418a56e Merge pull request #17 from jufm/ikvm
Implement remote KVM command
2015-10-07 13:19:26 -04:00
Juliana Motira 9b52e276cc Implement remote KVM command 2015-10-07 14:16:54 -03:00
Jarrod Johnson fa545eeaee Merge pull request #20 from jufm/ntpconfig
Implement NTP config command
2015-10-07 13:12:27 -04:00
Juliana Motira 5590b4138e Implement NTP config command 2015-10-07 13:58:38 -03:00
Jarrod Johnson 098c78558b Merge pull request #32 from jjohnson42/fixdeletenotification
Correct behavior when a node is deleted
2015-10-05 09:51:39 -04:00
Jarrod Johnson 57a3c6d287 Correct behavior when a node is deleted
When attribute notification is requested, node deletion was not
sent to the watchers.  Address the limitation by notifying on all
attributes for a deleted node.
2015-10-05 09:48:27 -04:00
Jarrod Johnson 3790984555 Merge pull request #31 from jjohnson42/autoinitconf
Have the config.conf autoinit if needed
2015-09-30 14:22:13 -04:00
Jarrod Johnson b88a135602 Have the config.conf autoinit if needed
If something makes a call out of sequence, attempt
to auto-init.
2015-09-30 14:21:01 -04:00
Jarrod Johnson 2115fb3f78 Merge pull request #30 from jjohnson42/replayoldlog
Fix erroneous data injection to log replay
2015-09-30 13:51:01 -04:00
Jarrod Johnson 1f8bc635a8 Fix erroneous data injection to log replay
When a rollover event was detected, the offset
of the rollover event itself was being read
from the rolled file erroneously.  Skip to
next loop iteration so that the metadata about
the rollover event is properly ignored in building
the text data buffer.
2015-09-30 13:48:04 -04:00
Jarrod Johnson f78a4d6074 Merge pull request #26 from jjohnson42/windowssupport
Support Windows management server
2015-09-30 10:19:54 -04:00
Jarrod Johnson d9ed98d58e Merge pull request #28 from jjohnson42/jsonrollevent
Change rollover event to be JSON
2015-09-30 10:19:34 -04:00
Jarrod Johnson db3320e2ec Merge pull request #29 from jjohnson42/moveleds
Move 'leds' to be a hardware sensor category
2015-09-28 14:44:24 -04:00
Jarrod Johnson a02f96710b Merge branch 'pr/23' 2015-09-28 13:41:26 -04:00
Jarrod Johnson 03de545e09 Merge branch 'master' into pr/23 2015-09-28 13:40:34 -04:00
Jarrod Johnson fcd0e523a0 Merge pull request #21 from jufm/add_dn
Implement domain name command
2015-09-28 13:17:11 -04:00
Jarrod Johnson 0ae2b7acc6 Move 'leds' to be a hardware sensor category
Rather than 'led' being distinct from 'hardware',
have 'leds' be a category of 'hardware' like 'fans'
or other things similarly hardware related.
2015-09-28 11:20:44 -04:00
Jarrod Johnson c50be7c3f2 Change rollover event to be JSON
The log format for other pieces of data is JSON.
Change the rollover event to be consistent.  Also
do not record the previous name of the log file,
as that isn't used, and the current filename is
likely to change when it too gets rolled over
so there's no practical use of knowing the
no longer valid name for the transaction.
2015-09-28 10:40:48 -04:00
Jarrod Johnson 45f62b5c05 Fix some issues around lock rework
Some scenarios were not accomodated correctly
as well as some references not set either.
2015-09-28 10:19:35 -04:00
Jarrod Johnson a0e6e0b5c6 Merge branch 'windowssupport' of git://github.com/jjohnson42/confluent into jjohnson42-windowssupport
Additionally, rework locking back to not require any extra files
2015-09-28 09:56:03 -04:00
Jarrod Johnson 9975d57d5e Implement Locking in Windows log code
Under windows, we can't use flock.  However we can
get locking using msvcrt using different, but related
semantics.  Imitate whole file locking by just locking
first byte.  We have to make sure we seek() to the same
place when locking and unlocking, as Windows requires
the offset to be same for both operations.
2015-09-25 16:59:42 -04:00
Jarrod Johnson c1bd46b22a Merge pull request #27 from jufm/encodedstring
Handling encoded input strings
2015-09-24 11:22:53 -04:00
Juliana Motira ff213916b6 Handling encoded input strings 2015-09-24 12:08:17 -03:00
Jarrod Johnson 5128a80c79 Merge pull request #22 from chenglch/log_rotation
Add log rotation support
2015-09-24 10:01:50 -04:00
chenglch dc436fda74 Add log rotation support
Add TimedAndSizeRotatingFileHandler which mixes together
the RotatingFileHandler and TimedRotatingFileHandler from
python logging module to process the log data.

Add logrollover event to track the renamed information, so
that console session can read the log data from current log
file and last renamed file.

Global configuration is used by the log handler. The format
of the log section in '/etc/confluent/service.cfg' is like:
[log]
when = m
backup_count = 3
max_bytes = 8192
utc = False
2015-09-23 23:36:46 -04:00
Jarrod Johnson ad20193309 Make confluent and confluentsrv.py identical
The two files should be identical.  confluentsrv.py exists
only because PyInstaller struggles unless the target is a
'py' file and does not have some namespace conflict with a
module.
2015-09-23 11:58:48 -04:00
Jarrod Johnson 0bd9b08be2 Remove extraneous print statements
Some debug output was in place, remove it.
2015-09-23 11:54:35 -04:00
Jarrod Johnson e9a31f52ae Merge branch 'windowssupport' of github.com:jjohnson42/confluent into windowssupport 2015-09-23 11:49:13 -04:00
Jarrod Johnson a3dcf88749 Fix log path for windows
Under windows, the log files were not being written
as expected.
2015-09-23 11:48:29 -04:00
Jarrod Johnson 765c15ed5b Revert 'confluentd' change
After further investigation, the rename to confluentd was not
needed (after massaging pathex and using pip to install rather
than distutils).
2015-09-23 11:48:27 -04:00
Jarrod Johnson 34960bff22 Add PyInstaller spec file
This facilitates a 'onedir' portable format for confluent server.
The 'onefile' mode couldn't be made to work, but this should suffice.
2015-09-23 11:48:23 -04:00
Jarrod Johnson 29417d935c Phase 2 of Windows compatibility
More work to try to enable confluent to be frozen by
pyinstaller
2015-09-23 11:48:20 -04:00
Jarrod Johnson b48cd8b685 Implement basic functionality under windows
Windows support by removing pid file, daemonizing, locking,
and other features.  Goal is to have a freezeable payload.
2015-09-23 11:48:17 -04:00
Jarrod Johnson a1a61dfdbd Merge pull request #25 from jjohnson42/master
Add shutdown to power state option
2015-09-21 16:38:37 -04:00
Juliana Motira 11d4628458 Add shutdown to power state option 2015-09-21 16:37:32 -04:00
Jarrod Johnson 4dcfaf7363 Merge pull request #24 from jufm/initscript
Make init script properly indicate error conditions.
2015-09-21 16:30:02 -04:00
Juliana Motira 655a0b4d17 Implement update network configuration 2015-09-17 10:03:20 -03:00
Allan Vidal 668f1c98b6 Make init script properly indicate error conditions. 2015-09-17 08:57:08 -03:00
Juliana Motira b1b4ee4634 Implement domain name command 2015-09-14 08:35:57 -03:00
Jarrod Johnson e207940e50 Merge pull request #19 from jufm/mci_fix
Fix MCI command
2015-09-09 15:15:03 -04:00
Juliana Motira e02dc74eb7 Fix MCI command 2015-09-09 11:47:46 -03:00
Jarrod Johnson 8114c3dc6a Merge pull request #18 from chenglch/bug/start_session
Fix 'start console session' command error
2015-09-09 10:37:05 -04:00
chenglch fde68e1320 Fix 'start console session' command error
Command "confetty start /nodes/<node>/console/session" can
not work correctly. This patch aims to add condition judgement
for this command in confetty.
2015-09-08 22:58:44 -04:00
Jarrod Johnson 32184d463d Merge pull request #16 from jufm/mci
Implement MCI command
2015-09-08 16:30:02 -04:00
Juliana Motira e7ff4fdd93 Implement MCI command 2015-09-08 12:24:47 -03:00
Jarrod Johnson 044def59ba Merge pull request #14 from jufm/reset
Implement reset bmc and NMI diag command
2015-08-27 10:18:05 -04:00
Lucio Seki 419c41e2cc Add NMI diag command 2015-08-27 10:29:42 -03:00
Juliana Motira c20f0dc2ae Implement reset command
Added a reset bmc command in nodes/<node_name>/configuration/management_controller/reset
2015-08-27 10:29:24 -03:00
Jarrod Johnson 455feb867d Merge pull request #13 from jufm/master
Changing LEDs structure
2015-08-24 13:53:12 -04:00
Juliana Motira dc0c7270a4 Changing LEDs structure 2015-08-24 14:47:07 -03:00
Jarrod Johnson ceee6173da Merge pull request #11 from jufm/master
Add firmware and LED status information.
2015-08-24 09:04:05 -04:00
Allan Vidal 2fb63a34ba Add LED status retrieval. 2015-08-24 09:13:32 -03:00
Juliana Motira 55c333b198 Add get firmware information.
Show OEM firmware information in /nodes/[node]/inventory/firmware/.
2015-08-24 09:12:44 -03:00
Jarrod Johnson 56455fe497 Merge pull request #12 from jufm/disableuser
Add an option to disable or enable an IPMI user
2015-08-20 13:36:11 -04:00
Juliana Motira 87552b9f03 Add an option to disable or enable an IPMI user 2015-08-20 11:51:29 -03:00
Jarrod Johnson 6ba7072aed Fix log path for windows
Under windows, the log files were not being written
as expected.
2015-08-13 13:46:48 -04:00
Jarrod Johnson f7b383b692 Revert 'confluentd' change
After further investigation, the rename to confluentd was not
needed (after massaging pathex and using pip to install rather
than distutils).
2015-08-12 13:06:49 -04:00
Jarrod Johnson a0e3dca856 Add PyInstaller spec file
This facilitates a 'onedir' portable format for confluent server.
The 'onefile' mode couldn't be made to work, but this should suffice.
2015-08-12 09:31:47 -04:00
Jarrod Johnson 90ad5829fc Phase 2 of Windows compatibility
More work to try to enable confluent to be frozen by
pyinstaller
2015-08-12 09:31:45 -04:00
Jarrod Johnson 669661b530 Implement basic functionality under windows
Windows support by removing pid file, daemonizing, locking,
and other features.  Goal is to have a freezeable payload.
2015-08-12 09:31:42 -04:00
Jarrod Johnson e17b9e98a8 Merge pull request #9 from jjohnson42/nodeeventlog
Implement 'nodeeventlog' command
2015-08-12 09:23:33 -04:00
Jarrod Johnson aef26abd56 Merge pull request #10 from jjohnson42/rejectbadnames
Prevent broken creation of nodes/groups
2015-08-12 09:22:59 -04:00
Jarrod Johnson 57bacd69c1 Prevent broken creation of nodes/groups
nodes and groups with '' name are invalid.  Avoid
terrible things by erroring out on attempts to even
try at the deepest layer that can guard.
2015-08-11 16:41:13 -04:00
Jarrod Johnson 88aa696d29 Implement 'nodeeventlog' command
This brings rough parity with reventlog.  Note that format does
change in this case, so full backwards compatibility is not retained.
2015-08-11 15:06:11 -04:00
Jarrod Johnson 548b71cd6c Merge pull request #8 from jufm/master
Implement list all IPMI users.
2015-08-06 09:32:17 -04:00
Juliana Motira c263c2eebb Implement list all IPMI users. 2015-08-06 10:28:21 -03:00
Jarrod Johnson 7a084bf538 Merge pull request #6 from jufm/master
Treating sensors that has status = Unavailable.
2015-07-29 16:49:46 -04:00
Juliana Motira cbf595df26 Treating sensors that has status = Unavailable. 2015-07-29 15:58:39 -03:00
Jarrod Johnson 680f60114e Merge pull request #5 from jjohnson42/master
Implement read network configuration
2015-07-29 08:22:10 -04:00
Jarrod Johnson ff52ad4740 Implement read network configuration
Read network configuration from the endpoint and present
under the management_controller configuration.
2015-07-29 08:17:41 -04:00
Jarrod Johnson 9626491c41 Merge pull request #4 from jufm/master
Update IPMI user management to enable partial update.
2015-07-28 13:58:28 -04:00
Juliana Motira 3f1d49c30b Update IPMI user management to enable partial update.
Improvements in InputCredential fields validation.
Check for all fields in InputCredential when operation = 'create'.
2015-07-28 14:08:59 -03:00
Jarrod Johnson 5f1ddc7f84 Merge pull request #3 from jjohnson42/master
Cleanup pid on abnormal exit
2015-07-28 10:55:18 -04:00
Jarrod Johnson 6e5a7e15d9 Cleanup pid on abnormal exit
If unlocking the security keys or loading the
plugins experiences an error, do not leave a
stale pid file behind.
2015-07-28 10:53:15 -04:00
Jarrod Johnson 5388f497d4 Merge pull request #2 from jufm/master
Adding IPMI user remove method
2015-07-22 09:40:17 -04:00
Juliana Motira cff997bd0b Adding IPMI user remove method
Enabling IPMI user removing method and fixing json generation when href is a number.
2015-07-22 10:21:14 -03:00
Jarrod Johnson e55f55677b Merge pull request #1 from jjohnson42/addstateids
Add numeric state_ids to sensor readings
2015-07-21 10:58:29 -04:00
Jarrod Johnson 905c41b021 Add numeric state_ids to sensor readings
A plugin may provide a numeric enumeration for
state ids.  Provide that for programmatic recognition
of events in addition to being able to combine the
available text.
2015-07-21 09:21:07 -04:00
Jarrod Johnson 19dd0539a9 Merge branch 'master' of ssh://git.code.sf.net/p/xcat/confluent 2015-07-15 14:35:31 -04:00
Victor Hu 4d1d016325 Add simple systemd service file for confluent server.
The sysvinit scripts were not returning the same informational
messages when run under systemctl as they were when run
under service.
2015-07-15 13:33:47 -04:00
Jarrod Johnson aa4783672d Fix unicode error in nodehealth output
If unicode data was in the incoming data, string.format would choke.  Make the
string template unicode so that it is able to cope.
2015-07-14 14:13:29 -04:00
Jarrod Johnson 859aad793d Collapse 'acknowledge' parameter to boolean, 'False' meaning False
When a client submits non-boolean data for acknowledge, coerce it into boolean in some reasonable way.
2015-07-14 13:28:54 -04:00
Jarrod Johnson f52eec6c19 Force Input data to be correct data
Provide a way to coerce input string data to
integer for alert parameters that are numeric.
2015-07-14 11:21:39 -04:00
Jarrod Johnson 24eb872090 Add 'type' field to sensors
In sensors/ and health/, put 'type' field in to clarify the sensor
reading category.
2015-07-13 10:15:03 -04:00
Jarrod Johnson d27df8fffc Provide specific error on locked credential store
When the credential store is locked, provide a specific message
and avoid triggering a trace log on a well characterized situation.
2015-07-08 16:47:58 -04:00
Jarrod Johnson 4aef8524e9 Implement specifying an external cfg key file
This allows the password to be protected by an external
file.  With this one can chain confluent's security to another
security mechanism.
2015-07-08 16:19:47 -04:00
Jarrod Johnson 14a9220acb Enable support for IPMI user management
Provide a framework for management of users on managed endpoints, and implement for IPMI plugin.

From Juliana Motira
2015-07-07 11:20:04 -04:00
Jarrod Johnson 49bff93eed Provide for configuration of the TLS remote socket
Refactor the http api configuration and have a section to
apply to the remote TLS socket as well.

From Lucio Seki
2015-07-06 13:48:01 -04:00
Jarrod Johnson 97c928350c Provide for configuration file specification of http listen
Establish a config file for certain configuration parameters that
control service startup and things that are best managed via out
of band configuration file and easiest to do with a restart.  For
now, implement control of http service binding.

From Lucio Seki
2015-07-02 13:23:48 -04:00
Jarrod Johnson 2d9df67272 Merge branch 'master' of ssh://git.code.sf.net/p/xcat/confluent 2015-07-01 15:14:28 -04:00
Jarrod Johnson e11a749fa4 Mandate that all http collections return children as list
Currently, an empty collection has no 'item' entry, a singleton value, or
a list depending on whether it has 0, 1, or more children.  Modify this so
that at least 1 and more children are consistent.
2015-07-01 15:14:04 -04:00
Jarrod Johnson 186929d217 Only conditionally import readline
If stdout is not a tty, do not import readline.  import
readline by itself can cause terminal control characters
to appear in the stdout.  Avoid this by only importing if
there seems to be a sign it is connected to a terminal.
2015-06-26 15:26:44 -04:00
Jarrod Johnson 86f66e9795 Implement a resource to decode alerts
This allows a standalone trap handler to request
decode from confluent via API interfaces.
2015-06-26 15:20:24 -04:00
Jarrod Johnson 5a610f23ca Fix numerous issues with the key protection
The passphrase protection had a number of issues and this corrects them.
2015-06-11 17:43:48 -04:00
Jarrod Johnson 3d5fa74f4f Add 'record_id' field to events
Provide method for plugins to pass through through an event identifier for
entries in the log.
2015-06-11 09:35:52 -04:00
Jarrod Johnson fefe1dc9e8 Add alert destination configuration
Alert destination configuration is now added.  This depends on
an as yet unreleased pyghmi capability.
2015-05-29 17:04:25 -04:00
Jarrod Johnson 7b02954da8 Reorder core resource list in alphabetical order
Clean up ordering to be in (roughly) alphabetical order
so it's a tad easier to follow.
2015-05-29 11:17:44 -04:00
Victor Hu 6ab1ae0c38 Implemented functions for start() and stop(). The restart case will
call the stop() and start() functions.

Impemented the status case so that 'service confluent status' will
return the state of confluent daemon.
2015-05-18 14:00:35 -04:00
Jarrod Johnson 6a1da9e84f Merge branch 'master' of ssh://git.code.sf.net/p/xcat/confluent 2015-05-15 16:52:14 -04:00
Jarrod Johnson c6d0d87ca9 Add event log to interface
Add a draft event log interface along with an ipmi implementation.
This requires current git master of pyghmi.  Release of pyghmi will
be done before this commit lands in a confluent release.
2015-05-15 16:49:52 -04:00
Victor Hu bfc7ea2b51 Add "shutdown" as a valid InputPowerMessage to prevent this Bad Request Error:
Error: Bad Request - shutdown is not one of reset,on,boot,off
2015-05-06 11:55:07 -04:00
Victor Hu 7baec5a69f In load_plugins, check for __init__.py files and avoid adding
them into the pluginmap
2015-05-05 16:36:04 -04:00
Jarrod Johnson 7cda6f7d6e Add support for fetching single component inventory
Like sensors, ipmi plugin now supports fetching individual component information.
2015-04-28 10:37:47 -04:00
Jarrod Johnson 2ab2fbda27 Make ipmi inventory more closely resemble sensors
Have the inventory data enumerate in one list rather
than a bunch of distinct lists.
2015-04-27 17:16:45 -04:00
Jarrod Johnson 6204628f43 Add support for inventory
Present 'inventory/hardware/all/' hierarchy.  Currently
only ipmi and 'all/all' works.  The data structure may be amended in
the very near future as well.
2015-04-27 16:57:52 -04:00
Jarrod Johnson b97cd79c3a Fix unhelpful timeout message
ipmi plugin was incorrectly constructing the message about timeout causing
the client having no idea what actually failed.
2015-03-26 16:13:54 -04:00
Jarrod Johnson d2f400d982 Fix messaging about configuration
A mistake was made in the messaging layer consistency fixes.  This caused the attributes plugin to fail
with unexpected errors.
2015-03-26 15:28:52 -04:00
Jarrod Johnson 46b7550a41 Fix confetty live reconnect behavior
If on a remote socket, socket.error could be thrown.  A mistake was
made where the Python2.x behavior of ',' on an except clause without
parenthesis misinterpreted socket.error as a variable name to store
instance of socket.gaierror.  Put Parentheses in to declare the desired
behavior.
2015-03-26 14:03:56 -04:00
Jarrod Johnson a28b67e9aa Have confluent log sessions out at exit
While the native perl threading object will do join() on exit,
eventlet's variant does not.  Fix this by manually hooking join() in
via atexit.  Since this is an eventlet specific thing, it makes sense
to work the issue in code that patches in eventlet rather than in pyghmi
itself.
2015-03-26 13:46:45 -04:00
Jarrod Johnson 4090dac50c Fix client having too short a timeout in remote TLS usage
There was a 5 second timeout to establish basic connectivity, but
was mistakenly extended beyond that.  Re-establish default timeout
behavior after connectivity established.
2015-03-26 13:27:57 -04:00
Jarrod Johnson c98d2e32d3 Have socket API shrug off client disconnect
If a client fails to stick around for data, shrug it
off rather than adding to stderr log.
2015-03-26 10:59:51 -04:00
Jarrod Johnson 2fe0425191 Have confluent client commands quietly exit on Ctrl-C
Pythons default handling of Ctrl-C is not in line with most command line utilities.
Wrap the exception and imitate more conventional behavior.
2015-03-26 10:38:44 -04:00
Jarrod Johnson 27437048a6 Fix nodehealth silently ignoring errors
nodehealth command did not act on errors.  Add a clause
to handle that data.
2015-03-26 10:17:03 -04:00
Jarrod Johnson 17b5d5a816 Fix issue where ipmi plugin would continue despite error
In changing to do multi-node, some flow was altered.  Where it would
formerly cease execution, the changes made it continue.  Add return
statements to match everywhere that return statements were effectively
removed.
2015-03-26 09:53:34 -04:00
Jarrod Johnson 26a969c41a Fix silent feedback to client in some ipmi scenarios
Provide specific feedback to client when possible.  When not possible,
at least get condition into the correct trace log and notify client of
condition.
2015-03-26 09:24:23 -04:00
Jarrod Johnson 093e9faec4 Fix various client issues
Usage messages when no noderange, consistent use of the environment variables for login,
fix nodehealth when a troublesome sensor has a value.
2015-03-25 17:19:58 -04:00
41 changed files with 2584 additions and 333 deletions
+1
View File
@@ -0,0 +1 @@
include confluent_env.sh
-1
View File
@@ -1 +0,0 @@
1.1
+43 -14
View File
@@ -41,19 +41,21 @@
# esc-( would interfere with normal esc use too much
# ~ I will not use for now...
import fcntl
import math
import getpass
import optparse
import os
import readline
import select
import shlex
import socket
import sys
import termios
import time
import tty
try:
import fcntl
import termios
import tty
except ImportError:
pass
exitcode = 0
consoleonly = False
@@ -73,8 +75,11 @@ conserversequence = '\x05c' # ctrl-e, c
oldtcattr = None
fd = sys.stdin
if fd.isatty():
oldtcattr = termios.tcgetattr(fd.fileno())
try:
if fd.isatty():
oldtcattr = termios.tcgetattr(fd.fileno())
except NameError:
pass
netserver = None
laststate = {}
@@ -248,6 +253,9 @@ def print_result(res):
attrstr = '%s=""' % key
elif type(res[key]) == list:
attrstr = '%s=%s' % (key, recurse_format(res[key]))
elif not isinstance(res[key], dict):
print '{0}: {1}'.format(key, res[key])
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
elif 'value' in res[key] and res[key]['value'] is None:
@@ -258,7 +266,10 @@ def print_result(res):
attrstr = '%s=""' % key
else:
sys.stdout.write('{0}: '.format(key))
print_result(res[key])
if isinstance(res[key], str) or isinstance(res[key], unicode):
print res[key]
else:
print_result(res[key])
continue
if res[key] is not None and 'inheritedfrom' in res[key]:
notes.append(
@@ -355,9 +366,11 @@ def do_command(command, server):
else:
sys.stderr.write("%s: command not found...\n" % argv[0])
def shutdown():
tlvdata.send(session.connection, {'operation': 'shutdown', 'path': '/'})
def createresource(args):
resname = args[0]
attribs = args[1:]
@@ -489,6 +502,18 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
inconsole = False
def get_session_node(shellargs):
# straight to node console
if len(shellargs) == 1 and ' ' not in shellargs[0]:
return shellargs[0]
if len(shellargs) == 2 and shellargs[0] == 'start':
args = [s for s in shellargs[1].split('/') if s]
if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \
args[3] == 'session':
return args[1]
return None
def conserver_command(filehandle, command):
# x - conserver has that as 'show baud', I am inclined to replace that with
# 'request exclusive'
@@ -576,7 +601,6 @@ def server_connect():
session.authenticate(username, passphrase)
while not session.authenticated:
username = raw_input("Name: ")
readline.clear_history()
passphrase = getpass.getpass("Passphrase: ")
session.authenticate(username, passphrase)
@@ -593,15 +617,20 @@ except socket.gaierror:
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
readline.set_completer(completer)
if sys.stdout.isatty():
import readline
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
readline.set_completer(completer)
doexit = False
inconsole = False
pendingcommand = ""
if len(shellargs) == 1 and ' ' not in shellargs[0]: # straight to node console
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
do_command("start /nodes/%s/console/session" % shellargs[0], netserver)
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
command = " ".join(shellargs)
@@ -642,7 +671,7 @@ while inconsole or not doexit:
try:
server_connect()
connected = True
except socket.gaierror, socket.error:
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
argparser = optparse.OptionParser(
usage="Usage: %prog [options] node",
epilog="Command sequences are available while connected to a console, hit "
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
"For example, ctrl-'e', then 'c', then '.' will exit the current "
"console")
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime as dt
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
try:
noderange = sys.argv[1]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> [clear]\n'.format(
sys.argv[0]))
sys.exit(1)
deletemode = False
if len(sys.argv) == 3:
if sys.argv[2] == 'clear':
deletemode = True
session = client.Command()
exitcode = 0
def format_event(evt):
retparts = []
if 'timestamp' in evt:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
dscparts = []
if 'component_type' in evt:
dscparts.append(evt['component_type'])
if 'component' in evt and evt['component'] is not None:
dscparts.append(evt['component'])
if 'event' in evt:
evttext = evt['event']
try:
if evttext.startswith(evt['component'] + ' - '):
evttext = evt['event'].replace(evt['component'] + ' - ', '')
except (KeyError, TypeError):
pass
dscparts.append(evttext)
retparts.append(' - '.join(dscparts))
return ' '.join(retparts)
if deletemode:
func = session.delete
else:
func = session.read
for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
if 'error' in rsp:
sys.stderr.write(rsp['error'] + '\n')
exitcode |= rsp['errorcode']
if 'databynode' in rsp:
nodedata = rsp['databynode']
for node in nodedata:
thisdata = nodedata[node]
if 'error' in thisdata:
sys.stderr.write('{0}: {1}\n'.format(node, thisdata['error']))
exitcode |= 1
if 'events' in thisdata:
evtdata = thisdata['events']
for evt in evtdata:
print '{0}: {1}'.format(node, format_event(evt))
+56 -36
View File
@@ -28,42 +28,62 @@ import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
noderange = sys.argv[1]
try:
noderange = sys.argv[1]
except IndexError:
sys.stderr.write('Usage: {0} <noderange>\n'.format(sys.argv[0]))
sys.exit(1)
session = client.Command()
exitcode = 0
healthbynode = {}
healthexplanations = {}
for health in session.read('/noderange/{0}/health/hardware'.format(noderange)):
if 'error' in health:
sys.stderr.write(health['error'] + '\n')
if 'errorcode' in health:
exitcode |= health['errorcode']
else:
exitcode |= 1
continue
if 'databynode' not in health:
continue
health = health['databynode']
for node in health:
if 'health' in health[node]:
healthbynode[node] = health[node]['health']['value']
if 'sensors' in health[node]:
healthexplanations[node] = []
for sensor in health[node]['sensors']:
explanation = sensor['name'] + ':'
if sensor['value'] is not None:
explanation += sensor['value']
if sensor['units'] is not None:
explanation += sensor['units']
if sensor['states']:
explanation += ','.join(sensor['states'])
healthexplanations[node].append(explanation)
if node in healthbynode and node in healthexplanations:
if healthexplanations[node]:
print('{0}: {1} ({2})'.format(
node, healthbynode[node],
','.join(healthexplanations[node])))
def main():
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
session = client.Command()
exitcode = 0
healthbynode = {}
healthexplanations = {}
for health in session.read(
'/noderange/{0}/health/hardware'.format(noderange)):
if 'error' in health:
sys.stderr.write(health['error'] + '\n')
if 'errorcode' in health:
exitcode |= health['errorcode']
else:
print('{0}: {1}'.format(node, healthbynode[node]))
exitcode |= 1
continue
if 'databynode' not in health:
continue
health = health['databynode']
for node in health:
if 'error' in health[node]:
sys.stderr.write('{0}: Error: {1}\n'.format(
node, health[node]['error']))
exitcode |= 1
if 'health' in health[node]:
healthbynode[node] = health[node]['health']['value']
if 'sensors' in health[node]:
healthexplanations[node] = []
for sensor in health[node]['sensors']:
explanation = sensor['name'] + ':'
if sensor['value'] is not None:
explanation += str(sensor['value'])
if sensor['units'] is not None:
explanation += sensor['units']
if sensor['states']:
explanation += ','
if sensor['states']:
explanation += ','.join(sensor['states'])
healthexplanations[node].append(explanation)
if node in healthbynode and node in healthexplanations:
if healthexplanations[node]:
print(u'{0}: {1} ({2})'.format(
node, healthbynode[node],
','.join(healthexplanations[node])))
else:
print('{0}: {1}'.format(node, healthbynode[node]))
try:
main()
except KeyboardInterrupt:
print('')
sys.exit(0)
+5 -1
View File
@@ -25,8 +25,12 @@ if path.startswith('/opt'):
import confluent.client as client
try:
noderange = sys.argv[1]
except IndexError:
sys.stderr.write('Usage: {0} <noderange> [on|off]\n'.format(sys.argv[0]))
sys.exit(1)
noderange = sys.argv[1]
identifystate = None
if len(sys.argv) > 2:
identifystate = sys.argv[2]
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2'
import optparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [list of attributes")
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
nodelist = '/nodes/'
session = client.Command()
exitcode = 0
if len(args) > 1:
seenattributes = set([])
for res in session.read('/noderange/{0}/attributes/all'.format(noderange)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in res['databynode']:
for attr in res['databynode'][node]:
seenattributes.add(attr)
currattr = res['databynode'][node][attr]
if attr in args[1:]:
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
node, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(node, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node, attr)
else:
attrout = '{0}: {1}:'.format(node, attr)
if options.blame:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
print attrout
if not exitcode:
for attr in args[1:]:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
+8 -1
View File
@@ -26,7 +26,14 @@ if path.startswith('/opt'):
import confluent.client as client
noderange = sys.argv[1]
try:
noderange = sys.argv[1]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> ([status|on|off|shutdown|boot|reset]\n'.format(
sys.argv[0]))
sys.exit(1)
setstate = None
if len(sys.argv) > 2:
if setstate == 'softoff':
+10 -3
View File
@@ -54,8 +54,11 @@ if options.numreadings:
repeatmode = options.numreadings
if options.interval is None:
options.interval = 1
noderange = args[0]
try:
noderange = args[0]
except IndexError:
argparser.print_usage()
sys.exit(1)
sensors = []
for sensorgroup in args[1:]:
for sensor in sensorgroup.split(','):
@@ -205,4 +208,8 @@ def main():
sensorpass(True)
main()
try:
main()
except KeyboardInterrupt:
print('')
sys.exit(0)
+9 -1
View File
@@ -26,10 +26,18 @@ if path.startswith('/opt'):
import confluent.client as client
noderange = sys.argv[1]
try:
noderange = sys.argv[1]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
sys.argv[0]))
sys.exit(1)
bootdev = None
if len(sys.argv) > 2:
bootdev = sys.argv[2]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
exitcode = 0
sys.exit(
+27 -18
View File
@@ -43,7 +43,7 @@ def _parseserver(string):
class Command(object):
def __init__(self, server="/var/run/confluent/api.sock"):
def __init__(self, server=None):
self.connection = None
if server is None:
if 'CONFLUENT_HOST' in os.environ:
@@ -62,6 +62,10 @@ class Command(object):
self.authenticated = True
else:
self.authenticated = False
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
username = os.environ['CONFLUENT_USER']
passphrase = os.environ['CONFLUENT_PASSPHRASE']
self.authenticate(username, passphrase)
def authenticate(self, username, password):
tlvdata.send(self.connection,
@@ -94,23 +98,27 @@ class Command(object):
def simple_noderange_command(self, noderange, resource, input=None,
key=None):
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), {ikey: input}):
rc = self.handle_results(ikey, rc, res)
return rc
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), {ikey: input}):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
print('')
return 0
@@ -146,6 +154,7 @@ class Command(object):
try:
self.connection.settimeout(5)
self.connection.connect(sa)
self.connection.settimeout(None)
except:
raise
self.connection.close()
+5
View File
@@ -38,6 +38,9 @@ def send(handle, data):
if isinstance(data, str):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
# if you don't have anything to say, don't say anything at all
return
if tl < 16777216:
# type for string is '0', so we don't need
# to xor anything in
@@ -76,6 +79,8 @@ def recv(handle):
# 4 byte tlv
dlen = tl & 16777215 # grab lower 24 bits
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
if dlen == 0:
return None
data = handle.recv(dlen)
while len(data) < dlen:
ndata = handle.recv(dlen - len(data))
+2
View File
@@ -0,0 +1,2 @@
PATH=/opt/confluent/bin:$PATH
export PATH
+2 -1
View File
@@ -7,5 +7,6 @@ setup(
author_email='jjohnson2@lenovo.com',
url='http://xcat.sf.net/',
packages=['confluent'],
scripts=['bin/confetty', 'bin/nodehealth', 'bin/nodeidentify', 'bin/nodepower', 'bin/nodesensors', 'bin/nodesetboot'],
scripts=['bin/confetty', 'bin/nodeconsole', 'bin/nodeeventlog', 'bin/nodehealth', 'bin/nodeidentify', 'bin/nodelist', 'bin/nodepower', 'bin/nodesensors', 'bin/nodesetboot'],
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
)
+1
View File
@@ -1 +1,2 @@
include sysvinit/*
include systemd/*
-1
View File
@@ -1 +0,0 @@
1.1
+5 -2
View File
@@ -22,14 +22,17 @@ path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
from confluent import main
import confluent.main
#import cProfile
#import time
#p = cProfile.Profile(time.clock)
#p.enable()
#try:
main.run()
import multiprocessing
if __name__ == '__main__':
multiprocessing.freeze_support()
confluent.main.run()
#except:
# pass
#p.disable()
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
#
# 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 sys
import os
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.main
#import cProfile
#import time
#p = cProfile.Profile(time.clock)
#p.enable()
#try:
import multiprocessing
if __name__ == '__main__':
multiprocessing.freeze_support()
confluent.main.run()
#except:
# pass
#p.disable()
#p.print_stats(sort='cumulative')
#p.print_stats(sort='time')
+59
View File
@@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This handles incoming unsolicited alerts over the network. For the moment
# we'll link into ipmi.py to do PET alerts, with the assumption that more
# typical .mib based handling will be used for other events and confluent's
# event service is to handle the peculiarities of an IPMI PET. In the future
# it may make sense to extend this in a more general case, but that rework can
# be deferred for now.
# Phase 1 is to be facilitating some application doing http calls to get help
# decoding data.
# Phase 2 is to be able to bind a port and have snmptrapd just forward the
# packet rather than have something block snmptrapd at all for things confluent
# can handle.
__author__ = 'jjohnson2'
import confluent.exceptions as exc
import confluent.lookuptools as lookuptools
import confluent.core
def decode_alert(varbinds, configmanager):
"""Decode an SNMP alert for a server
Given the agentaddr, OID for the trap, and a dict of varbinds,
ascertain the node identity and then request a decode
:param varbinds: A dictionary of OID to value varbinds. Also supported
are special keywords 'enterprise' and 'specificTrap' for
SNMPv1 traps.
"""
try:
agentaddr = varbinds['.1.3.6.1.6.3.18.1.3.0']
except KeyError:
agentaddr = varbinds['1.3.6.1.6.3.18.1.3.0']
node = lookuptools.node_by_manager(agentaddr)
if node is None:
raise exc.InvalidArgumentException(
'Unable to find a node with specified manager')
return confluent.core.handle_path(
'/nodes/{0}/events/hardware/decode'.format(node), 'update',
configmanager, varbinds, autostrip=False)
+6 -1
View File
@@ -26,7 +26,10 @@ import Crypto.Protocol.KDF as KDF
import hashlib
import hmac
import multiprocessing
import PAM
try:
import PAM
except ImportError:
pass
import time
_pamservice = 'confluent'
@@ -161,6 +164,8 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
pammy.acct_mgmt()
del pammy
return authorize(user, element, tenant, skipuserobj=False)
except NameError:
pass
except PAM.error:
if credobj.haspam:
return None
+69
View File
@@ -0,0 +1,69 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
#
# 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 defines config variable to store the global configuration for confluent
import ConfigParser
import os
_config = None
def init_config():
global _config
configfile = "/etc/confluent/service.cfg"
if os.name == 'nt':
configfile = os.path.join(os.getenv('SystemDrive'), '\\ProgramData',
'confluent', 'cfg', 'service.cfg')
_config = ConfigParser.ConfigParser()
_config.read(configfile)
def get_config():
if _config is None:
init_config()
return _config
def get_int_option(section, option):
if _config is None:
init_config()
try:
return _config.getint(section, option)
except (
ConfigParser.NoSectionError, ConfigParser.NoOptionError,
ValueError):
return None
def get_boolean_option(section, option):
if _config is None:
init_config()
try:
return _config.getboolean(section, option)
except (
ConfigParser.NoSectionError, ConfigParser.NoOptionError,
ValueError):
return None
def get_option(section, option):
if _config is None:
init_config()
try:
return _config.get(section, option)
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
return None
@@ -67,6 +67,7 @@ import base64
import confluent.config.attributes as allattributes
import confluent.log
import confluent.util
import confluent.exceptions as exc
import copy
import cPickle
import errno
@@ -78,6 +79,7 @@ import re
import string
import sys
import threading
import traceback
_masterkey = None
@@ -104,22 +106,26 @@ def _derive_keys(password, salt):
lambda p, s: HMAC.new(p, s, SHA256).digest())
finalkey = KDF.PBKDF2(tmpkey, salt, 32, 50000,
lambda p, s: HMAC.new(p, s, SHA256).digest())
return finalkey[:32], finalkey[32:]
return finalkey[:16], finalkey[16:]
def _get_protected_key(keydict, password):
if keydict['unencryptedvalue']:
def _get_protected_key(keydict, password, paramname):
if password and 'unencryptedvalue' in keydict:
set_global(paramname, _format_key(
keydict['unencryptedvalue'],
password=password))
if 'unencryptedvalue' in keydict:
return keydict['unencryptedvalue']
# TODO(jbjohnso): check for TPM sealing
if 'passphraseprotected' in keydict:
if password is None:
raise Exception("Passphrase protected secret requires password")
for pp in keydict['passphraseprotected']:
salt = pp[0]
privkey, integkey = _derive_keys(password, salt)
return decrypt_value(pp[1:], key=privkey, integritykey=integkey)
raise exc.LockedCredentials("Passphrase protected secret requires password")
pp = keydict['passphraseprotected']
salt = pp[0]
privkey, integkey = _derive_keys(password, salt)
return decrypt_value(pp[1:], key=privkey, integritykey=integkey)
else:
raise Exception("No available decryption key")
raise exc.LockedCredentials("No available decryption key")
def _format_key(key, password=None):
@@ -127,7 +133,7 @@ def _format_key(key, password=None):
salt = os.urandom(32)
privkey, integkey = _derive_keys(password, salt)
cval = crypt_value(key, key=privkey, integritykey=integkey)
return {"passphraseprotected": cval}
return {"passphraseprotected": (salt,) + cval}
else:
return {"unencryptedvalue": key}
@@ -138,7 +144,7 @@ def init_masterkey(password=None):
cfgn = get_global('master_privacy_key')
if cfgn:
_masterkey = _get_protected_key(cfgn, password=password)
_masterkey = _get_protected_key(cfgn, password, 'master_privacy_key')
else:
_masterkey = os.urandom(32)
set_global('master_privacy_key', _format_key(
@@ -146,7 +152,8 @@ def init_masterkey(password=None):
password=password))
cfgn = get_global('master_integrity_key')
if cfgn:
_masterintegritykey = _get_protected_key(cfgn, password=password)
_masterintegritykey = _get_protected_key(cfgn, password,
'master_integrity_key')
else:
_masterintegritykey = os.urandom(64)
set_global('master_integrity_key', _format_key(
@@ -155,15 +162,18 @@ def init_masterkey(password=None):
def decrypt_value(cryptvalue,
key=_masterkey,
integritykey=_masterintegritykey):
key=None,
integritykey=None):
iv, cipherdata, hmac = cryptvalue
if _masterkey is None or _masterintegritykey is None:
init_masterkey()
check_hmac = HMAC.new(_masterintegritykey, cipherdata, SHA256).digest()
if key is None and integritykey is None:
if _masterkey is None or _masterintegritykey is None:
init_masterkey()
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(_masterkey, AES.MODE_CBC, iv)
decrypter = AES.new(key, AES.MODE_CBC, iv)
value = decrypter.decrypt(cipherdata)
padsize = ord(value[-1])
pad = value[-padsize:]
@@ -176,13 +186,14 @@ def decrypt_value(cryptvalue,
def crypt_value(value,
key=_masterkey,
integritykey=_masterintegritykey):
key=None,
integritykey=None):
# 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:
init_masterkey()
if _masterkey is None or _masterintegritykey is None:
init_masterkey()
key = _masterkey
integritykey = _masterintegritykey
iv = os.urandom(16)
@@ -405,7 +416,11 @@ def hook_new_configmanagers(callback):
class ConfigManager(object):
_cfgdir = "/etc/confluent/cfg/"
if os.name == 'nt':
_cfgdir = os.path.join(
os.getenv('SystemDrive'), '\\ProgramData', 'confluent', 'cfg')
else:
_cfgdir = "/etc/confluent/cfg"
_cfgwriter = None
_writepending = False
_syncrunning = False
@@ -864,6 +879,9 @@ class ConfigManager(object):
def set_group_attributes(self, attribmap, autocreate=False):
changeset = {}
for group in attribmap.iterkeys():
if group == '':
raise ValueError('"{0}" is not a valid group name'.format(
group))
if not autocreate and group not in self._cfgstore['nodegroups']:
raise ValueError("{0} group does not exist".format(group))
for attr in attribmap[group].iterkeys():
@@ -973,7 +991,16 @@ class ConfigManager(object):
if node not in attribwatchers:
continue
attribwatcher = attribwatchers[node]
for attrname in nodeattrs[node].iterkeys():
# usually, we will only look at the specific attribute keys that
# have had change flagged, so set up to iterate through only those
checkattrs = nodeattrs[node]
if '_nodedeleted' in nodeattrs[node]:
# in the case of a deleted node, we want to iterate through
# *all* attributes that the node might have had set prior
# to deletion, to make all watchers aware of the removed
# node and take appropriate action
checkattrs = attribwatcher
for attrname in checkattrs:
if attrname not in attribwatcher:
continue
for notifierid in attribwatcher[attrname].iterkeys():
@@ -1006,6 +1033,9 @@ class ConfigManager(object):
watcher(added=[], deleting=nodes, 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')
if node in self._cfgstore['nodes']:
self._sync_groups_to_node(node=node, groups=[],
@@ -1070,6 +1100,8 @@ class ConfigManager(object):
# this mitigates risk of arguments being partially applied
for node in attribmap.iterkeys():
node = node.encode('utf-8')
if node == '':
raise ValueError('"{0}" is not a valid node name'.format(node))
if autocreate is False and node not in self._cfgstore['nodes']:
raise ValueError("node {0} does not exist".format(node))
for attrname in attribmap[node].iterkeys():
@@ -1192,15 +1224,15 @@ class ConfigManager(object):
global _cfgstore
_cfgstore = {}
rootpath = cls._cfgdir
_load_dict_from_dbm(['globals'], rootpath + "/globals")
_load_dict_from_dbm(['globals'], os.path.join(rootpath, "globals"))
for confarea in _config_areas:
_load_dict_from_dbm(['main', confarea], rootpath + "/" + confarea)
_load_dict_from_dbm(['main', confarea], os.path.join(rootpath, confarea))
try:
for tenant in os.listdir(rootpath + '/tenants/'):
for tenant in os.listdir(os.path.join(rootpath, 'tenants')):
for confarea in _config_areas:
_load_dict_from_dbm(
['main', tenant, confarea],
"%s/%s/%s" % (rootpath, tenant, confarea))
os.path.join(rootpath, tenant, confarea))
except OSError:
pass
@@ -1231,7 +1263,7 @@ class ConfigManager(object):
dirtyglobals = copy.deepcopy(_cfgstore['dirtyglobals'])
del _cfgstore['dirtyglobals']
_mkpath(cls._cfgdir)
globalf = dbm.open(cls._cfgdir + "/globals", 'c', 384) # 0600
globalf = dbm.open(os.path.join(cls._cfgdir, "globals"), 'c', 384) # 0600
try:
for globalkey in dirtyglobals:
if globalkey in _cfgstore['globals']:
@@ -1252,11 +1284,11 @@ class ConfigManager(object):
pathname = cls._cfgdir
currdict = _cfgstore['main']
else:
pathname = cls._cfgdir + '/tenants/' + tenant + '/'
pathname = os.path.join(cls._cfgdir, 'tenants', tenant)
currdict = _cfgstore['tenant'][tenant]
for category in dkdict.iterkeys():
_mkpath(pathname)
dbf = dbm.open(pathname + category, 'c', 384) # 0600
dbf = dbm.open(os.path.join(pathname, category), 'c', 384) # 0600
try:
for ck in dkdict[category]:
if ck not in currdict[category]:
@@ -1310,7 +1342,8 @@ def dump_db_to_directory(location, password, redact=None):
cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact))
cfgfile.write('\n')
try:
for tenant in os.listdir(ConfigManager._cfgdir + '/tenants/'):
for tenant in os.listdir(
os.path.join(ConfigManager._cfgdir, '/tenants/')):
with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile:
cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json(
redact=redact))
+7 -1
View File
@@ -347,6 +347,9 @@ class _ConsoleHandler(object):
if data == conapi.ConsoleEvent.Disconnect:
self._got_disconnected()
return
elif data == '':
# ignore empty strings from a cconsole provider
return
if '\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
@@ -362,7 +365,10 @@ class _ConsoleHandler(object):
eventdata |= 2
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.buffer += data
if isinstance(data, bytearray) or isinstance(data, bytes):
self.buffer += data
else:
self.buffer += data.encode('utf-8')
#TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
+126 -30
View File
@@ -33,18 +33,23 @@
# functions. Console is special and just get's passed through
# see API.txt
import confluent.alerts as alerts
import confluent.config.attributes as attrscheme
import confluent.interface.console as console
import confluent.exceptions as exc
import confluent.messages as msg
import confluent.noderange as noderange
import confluent.shellmodule as shellmodule
try:
import confluent.shellmodule as shellmodule
except ImportError:
pass
import itertools
import os
import sys
pluginmap = {}
def seek_element(currplace, currkey):
try:
return currplace[currkey]
@@ -55,10 +60,11 @@ def seek_element(currplace, currkey):
return currplace
raise
def nested_lookup(nestdict, key):
try:
return reduce(seek_element, key, nestdict)
except TypeError as e:
except TypeError:
raise exc.NotFoundException("Invalid element requested")
@@ -72,7 +78,7 @@ def load_plugins():
if not os.path.isdir(plugindir):
continue
sys.path.append(plugindir)
#two passes, to avoid adding both py and pyc files
# two passes, to avoid adding both py and pyc files
for plugin in os.listdir(plugindir):
if plugin.startswith('.'):
continue
@@ -80,7 +86,7 @@ def load_plugins():
if plugtype == '.sh':
pluginmap[plugin] = shellmodule.Plugin(
os.path.join(plugindir, plugin + '.sh'))
else:
elif "__init__" not in plugin:
plugins.add(plugin)
for plugin in plugins:
tmpmod = __import__(plugin)
@@ -91,13 +97,14 @@ def load_plugins():
pluginmap[plugin] = tmpmod
rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/']
rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/', 'events/']
class PluginRoute(object):
def __init__(self, routedict):
self.routeinfo = routedict
class PluginCollection(object):
def __init__(self, routedict):
self.routeinfo = routedict
@@ -105,21 +112,81 @@ class PluginCollection(object):
# _ prefix indicates internal use (e.g. special console scheme) and should not
# be enumerated in any collection
noderesources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
'boot': {
'nextdevice': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'configuration': {
'management_controller': {
'alerts': {
'destinations': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'users': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'reset': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'identifier': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'domain_name': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'ntp': {
'enabled': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'servers': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
}
},
'_console': {
'session': PluginRoute({
'pluginattrs': ['console.method'],
}),
},
'console': {
#this is a dummy value, http or socket must handle special
# this is a dummy value, http or socket must handle special
'session': PluginRoute({}),
},
'power': {
'state': PluginRoute({
'license': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'events': {
'hardware': {
'log': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'decode': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
@@ -130,16 +197,26 @@ noderesources = {
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'boot': {
'nextdevice': PluginRoute({
'inventory': {
'hardware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'firmware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'power': {
'state': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
'sensors': {
'hardware': {
'all': PluginCollection({
@@ -158,7 +235,12 @@ noderesources = {
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'leds': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
}
@@ -313,6 +395,7 @@ def enumerate_collections(collections):
def handle_nodegroup_request(configmanager, inputdata,
pathcomponents, operation):
iscollection = False
routespec = None
if len(pathcomponents) < 2:
if operation == "create":
inputdata = msg.InputAttributes(pathcomponents, inputdata)
@@ -337,10 +420,10 @@ def handle_nodegroup_request(configmanager, inputdata,
if iscollection:
if operation == "delete":
return delete_nodegroup_collection(pathcomponents,
configmanager)
configmanager)
elif operation == "retrieve":
return enumerate_nodegroup_collection(pathcomponents,
configmanager)
configmanager)
else:
raise Exception("TODO")
plugroute = routespec.routeinfo
@@ -356,7 +439,7 @@ def handle_nodegroup_request(configmanager, inputdata,
def handle_node_request(configmanager, inputdata, operation,
pathcomponents):
pathcomponents, autostrip=True):
iscollection = False
routespec = None
if pathcomponents[0] == 'noderange':
@@ -397,7 +480,8 @@ def handle_node_request(configmanager, inputdata, operation,
except TypeError:
allnodes.sort()
return iterate_collections(allnodes)
if isnoderange and len(pathcomponents) == 3 and pathcomponents[2] == 'nodes':
if (isnoderange and len(pathcomponents) == 3 and
pathcomponents[2] == 'nodes'):
# this means that it's a list of relevant nodes
nodes = list(nodes)
try:
@@ -427,10 +511,10 @@ def handle_node_request(configmanager, inputdata, operation,
passvalues = []
plugroute = routespec.routeinfo
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes)
pathcomponents, operation, inputdata, nodes, isnoderange)
if 'handler' in plugroute: # fixed handler definition, easy enough
hfunc = getattr(pluginmap[plugroute['handler']], operation)
passvalue =hfunc(
passvalue = hfunc(
nodes=nodes, element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata)
@@ -455,19 +539,20 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
for hfunc in nodesbyhandler.iterkeys():
for hfunc in nodesbyhandler:
passvalues.append(hfunc(
nodes=nodesbyhandler[hfunc], element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata))
if isnoderange:
if isnoderange or not autostrip:
return itertools.chain(*passvalues)
elif isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
def handle_path(path, operation, configmanager, inputdata=None):
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
"""Given a full path request, return an object.
The plugins should generally return some sort of iterator.
@@ -482,17 +567,17 @@ def handle_path(path, operation, configmanager, inputdata=None):
return enumerate_collections(rootcollections)
elif pathcomponents[0] == 'noderange':
return handle_node_request(configmanager, inputdata, operation,
pathcomponents)
pathcomponents, autostrip)
elif pathcomponents[0] == 'nodegroups':
return handle_nodegroup_request(configmanager, inputdata,
pathcomponents,
operation)
pathcomponents,
operation)
elif pathcomponents[0] == 'nodes':
#single node request of some sort
# single node request of some sort
return handle_node_request(configmanager, inputdata,
operation, pathcomponents)
operation, pathcomponents, autostrip)
elif pathcomponents[0] == 'users':
#TODO: when non-administrator accounts exist,
# TODO: when non-administrator accounts exist,
# they must only be allowed to see their own user
try:
user = pathcomponents[1]
@@ -514,5 +599,16 @@ def handle_path(path, operation, configmanager, inputdata=None):
pathcomponents, operation, inputdata)
update_user(user, inputdata.attribs, configmanager)
return show_user(user, configmanager)
elif pathcomponents[0] == 'events':
try:
element = pathcomponents[1]
except IndexError:
if operation != 'retrieve':
raise exc.InvalidArgumentException('Target is read-only')
return (msg.ChildCollection('decode'),)
if element != 'decode':
raise exc.NotFoundException()
if operation == 'update':
return alerts.decode_alert(inputdata, configmanager)
else:
raise exc.NotFoundException()
+10 -1
View File
@@ -42,6 +42,11 @@ class TargetEndpointBadCredentials(ConfluentException):
# failed
pass
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
pass
class ForbiddenRequest(ConfluentException):
# The client request is not allowed by authorization engine
@@ -51,4 +56,8 @@ class ForbiddenRequest(ConfluentException):
class NotImplementedException(ConfluentException):
# The current configuration/plugin is unable to perform
# the requested task. http code 501
pass
pass
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
pass
+60 -14
View File
@@ -25,7 +25,7 @@ import confluent.exceptions as exc
import confluent.log as log
import confluent.messages
import confluent.core as pluginapi
import confluent.tlvdata as tlvdata
import confluent.tlvdata
import confluent.util as util
import copy
import eventlet
@@ -36,6 +36,7 @@ import time
import urlparse
import eventlet.wsgi
#scgi = eventlet.import_patched('flup.server.scgi')
tlvdata = confluent.tlvdata
auditlog = None
@@ -103,9 +104,36 @@ def node_creation_resources():
desc=attribs.node[attr]['description']).html() + '<br>\n'
def user_creation_resources():
credential = {
'uid': {
'description': (''),
},
'username': {
'description': (''),
},
'password': {
'description': (''),
},
'privilege_level': {
'description': (''),
},
}
for attr in sorted(credential.iterkeys()):
if attr == "password":
yield confluent.messages.CryptedAttributes(
kv={attr: None},
desc=credential[attr]['description']).html() + '<br>\n'
else:
yield confluent.messages.Attributes(
kv={attr: None},
desc=credential[attr]['description']).html() + '<br>\n'
create_resource_functions = {
'/nodes/': node_creation_resources,
'/groups/': group_creation_resources,
'nodes': node_creation_resources,
'groups': group_creation_resources,
'users': user_creation_resources,
}
@@ -399,12 +427,15 @@ def resourcehandler_backend(env, start_response):
except exc.InvalidArgumentException as e:
start_response('400 Bad Request - ' + str(e), headers)
yield '400 - Bad Request - ' + str(e)
except exc.TargetEndpointUnreachable:
except exc.TargetEndpointUnreachable as tu:
start_response('504 Unreachable Target', headers)
yield '504 - Unreachable Target'
yield '504 - Unreachable Target - ' + str(tu)
except exc.TargetEndpointBadCredentials:
start_response('502 Bad Credentials', headers)
yield '502 - Bad Credentials'
except exc.LockedCredentials:
start_response('500 Locked credential store', headers)
yield '500 - Credential store locked'
except exc.NotImplementedException:
start_response('501 Not Implemented', headers)
yield '501 Not Implemented'
@@ -429,7 +460,6 @@ def _assemble_html(responses, resource, querydict, url, extension):
iscollection = True
yield '<a rel="collection" href="../{0}">../{0}</a><br>'.format(
extension)
else:
iscollection = False
yield '<a rel="collection" href="./{0}">./{0}</a><br>'.format(
@@ -445,11 +475,15 @@ def _assemble_html(responses, resource, querydict, url, extension):
if iscollection:
# localpath = url[:-2] (why was this here??)
try:
if url == '/users/':
return
firstpass = True
for y in create_resource_functions[url]():
module = url.split('/')
if not module:
return
for y in create_resource_functions[module[-2]]():
if firstpass:
yield "<hr>Define new resource in %s:<BR>" % \
url.split("/")[-2]
yield "<hr>Define new resource in %s:<BR>" % module[-2]
firstpass = False
yield y
yield ('<input value="create" name="restexplorerop" type="submit">'
@@ -482,12 +516,16 @@ def _assemble_json(responses, resource, url, extension):
haldata = rsp.raw()
for hk in haldata.iterkeys():
if 'href' in haldata[hk]:
if isinstance(haldata[hk]['href'], int):
haldata[hk]['href'] = str(haldata[hk]['href'])
haldata[hk]['href'] += extension
if hk in links:
if isinstance(links[hk], list):
links[hk].append(haldata[hk])
else:
links[hk] = [links[hk], haldata[hk]]
elif hk == 'item':
links[hk] = [haldata[hk],]
else:
links[hk] = haldata[hk]
else:
@@ -499,14 +537,20 @@ def _assemble_json(responses, resource, url, extension):
else:
rspdata[dk] = [rspdata[dk], rsp[dk]]
else:
rspdata[dk] = rsp[dk]
if dk == 'databynode':
# a quirk, databynode suggests noderange
# multi response. This should *always* be a list,
# even if it will be length 1
rspdata[dk] = [rsp[dk]]
else:
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')
def serve():
def serve(bind_host, bind_port):
# TODO(jbjohnso): move to unix socket and explore
# either making apache deal with it
# or just supporting nginx or lighthttpd
@@ -518,20 +562,22 @@ def serve():
#also, the potential for direct http can be handy
#todo remains unix domain socket for even http
eventlet.wsgi.server(
eventlet.listen(('::', 4005, 0, 0), family=socket.AF_INET6),
eventlet.listen((bind_host, bind_port, 0, 0), family=socket.AF_INET6),
resourcehandler, log=False, log_output=False, debug=False)
class HttpApi(object):
def __init__(self):
def __init__(self, bind_host=None, bind_port=None):
self.server = None
self.bind_host = bind_host or '::'
self.bind_port = bind_port or 4005
def start(self):
global auditlog
global tracelog
tracelog = log.Logger('trace')
auditlog = log.Logger('audit')
self.server = eventlet.spawn(serve)
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port)
_cleaner = eventlet.spawn(_sessioncleaner)
@@ -28,7 +28,7 @@ class Console(object):
"""This is the class defining the interface a console plugin must return
for the _console/session element"""
def __init__(self, node, config):
raise NotImplementedError("Subclassing required")
return
def connect(self, callback):
raise NotImplementedError("Subclassing required")
@@ -36,13 +36,10 @@ class Console(object):
def write(self, data):
raise NotImplementedError("Subclassing required")
def wait_for_data(self, timeout=600):
raise NotImplementedError("Subclassing required")
def send_break(self):
"""This function is how a plugin should implement sending a break to
the remote console"""
pass
return
def ping(self):
"""This function is a hint to the console plugin that now would be a
@@ -54,4 +51,4 @@ class Console(object):
as well, so consoles can schedule a health check to run at this time.
No return is expected, any error condition can be reported by sending
ConsoleEvent.Disconnect, just like normal."""
pass
return
+472 -38
View File
@@ -61,14 +61,34 @@
import collections
import confluent.config.configmanager
import confluent.config.conf as conf
import confluent.exceptions as exc
import eventlet
import fcntl
import glob
import json
import os
import re
import stat
import struct
import time
import traceback
try:
from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_SH
except ImportError:
if os.name == 'nt':
import msvcrt
LOCK_SH = msvcrt.LK_LOCK # no shared, degrade to exclusive
LOCK_EX = msvcrt.LK_LOCK
LOCK_UN = msvcrt.LK_UNLCK
def flock(file, flag):
oldoffset = file.tell()
file.seek(0)
msvcrt.locking(file.fileno(), flag, 1)
file.seek(oldoffset)
else:
raise
# on conserving filehandles:
# upon write, if file not open, open it for append
# upon write, schedule/reschedule closing filehandle in 15 seconds
@@ -80,14 +100,14 @@ import traceback
# if that happens, warn to have user increase ulimit for optimal
# performance
MIDNIGHT = 24 * 60 * 60
_loggers = {}
class Events(object):
(
undefined, clearscreen, clientconnect, clientdisconnect,
consoledisconnect, consoleconnect, stacktrace
) = range(7)
consoledisconnect, consoleconnect, stacktrace, logrollover
) = range(8)
logstr = {
2: 'connection by ',
3: 'disconnection by ',
@@ -98,6 +118,371 @@ class DataTypes(object):
text, dictionary, console, event = range(4)
class RollingTypes(object):
no_rolling, size_rolling, time_rolling = range(3)
class BaseRotatingHandler(object):
def __init__(self, filepath, logname):
"""
Use the specified filename for streamed logging
"""
self.filepath = filepath
self.textpath = os.path.join(self.filepath, logname)
self.binpath = os.path.join(self.filepath, logname + ".cbl")
self.textfile = None
self.binfile = None
def open(self):
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
self.textfile.seek(0, 2)
if self.binfile is None:
self.binfile = open(self.binpath, mode='ab')
self.binfile.seek(0, 2)
return self.textfile, self.binfile
def try_emit(self, binrecord, textrecord):
"""
Emit a record.
Output the record to the file, catering for rollover as described
in doRollover().
"""
rolling_type = self.shouldRollover(binrecord, textrecord)
if rolling_type:
flock(self.textfile, LOCK_UN)
return self.doRollover(rolling_type)
return None
def emit(self, binrecord, textrecord):
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
if self.binfile is None:
self.binfile = open(self.binpath, mode='ab')
self.textfile.write(textrecord)
self.binfile.write(binrecord)
self.textfile.flush()
self.binfile.flush()
def get_textfile_offset(self, data_len):
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
return self.textfile.tell() + data_len
def close(self):
if self.textfile:
if not self.textfile.closed:
self.textfile.close()
self.textfile = None
if self.binfile:
if not self.binfile.closed:
self.binfile.close()
self.binfile = None
class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
"""
Handler for logging to a file, rotating the log file at certain timed
intervals.
If backupCount is > 0, when rollover is done, no more than backupCount
files are kept - the oldest ones are deleted.
"""
def __init__(self, filepath, logname, interval=1):
BaseRotatingHandler.__init__(self, filepath, logname)
try:
self.when = conf.get_option('log', 'when').upper()
except (AttributeError):
self.when = 'D'
self.backupCount = conf.get_int_option('log', 'backup_count') or 3
self.maxBytes = conf.get_int_option(
'log','max_bytes') or 4 * 1024 * 1024 * 1024
if self.maxBytes < 8192:
raise exc.GlobalConfigError("The minimum value of max_bytes "
"of log rolling size in the log "
"section should larger than 8192.")
self.utc = conf.get_boolean_option('log', 'utc') or False
# Calculate the real rollover interval, which is just the number of
# seconds between rollovers. Also set the filename suffix used when
# a rollover occurs. Current 'when' events supported:
# S - Seconds
# M - Minutes
# H - Hours
# D - Days
# midnight - roll over at midnight
# W{0-6} - roll over on a certain day; 0 - Monday
#
# Case of the 'when' specifier is not important; lower or upper case
# will work.
if self.when == 'S':
self.interval = 1 # one second
self.suffix = "%Y-%m-%d_%H-%M-%S"
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.{0,1}\d*$"
elif self.when == 'M':
self.interval = 60 # one minute
self.suffix = "%Y-%m-%d_%H-%M"
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}-\d{2}\.{0,1}\d*$"
elif self.when == 'H':
self.interval = 60 * 60 # one hour
self.suffix = "%Y-%m-%d_%H"
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}$"
elif self.when == 'D' or self.when == 'MIDNIGHT':
self.interval = 60 * 60 * 24 # one day
self.suffix = "%Y-%m-%d"
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}\.{0,1}\d*$"
elif self.when.startswith('W'):
self.interval = 60 * 60 * 24 * 7 # one week
if len(self.when) != 2:
raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
if self.when[1] < '0' or self.when[1] > '6':
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
self.dayOfWeek = int(self.when[1])
self.suffix = "%Y-%m-%d"
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}\.{0,1}\d*$"
else:
raise ValueError("Invalid rollover interval specified: %s" % self.when)
self.extMatch = re.compile(self.extMatch)
self.interval = self.interval * interval # multiply by units requested
# Note: Get the modify time of text log file to calculate the
# rollover time
if os.path.exists(self.textpath):
t = os.stat(self.textpath)[stat.ST_MTIME]
else:
t = int(time.time())
self.rolloverAt = self.computeRollover(t)
self.sizeRollingCount = 0
self.initSizeRollingCount()
def computeRollover(self, currentTime):
"""
Work out the rollover time based on the specified time.
"""
result = currentTime + self.interval
# If we are rolling over at midnight or weekly, then the interval is already known.
# What we need to figure out is WHEN the next interval is. In other words,
# if you are rolling over at midnight, then your base interval is 1 day,
# but you want to start that one day clock at midnight, not now. So, we
# have to fudge the rolloverAt value in order to trigger the first rollover
# at the right time. After that, the regular interval will take care of
# the rest. Note that this code doesn't care about leap seconds. :)
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
# This could be done with less code, but I wanted it to be clear
if self.utc:
t = time.gmtime(currentTime)
else:
t = time.localtime(currentTime)
currentHour = t[3]
currentMinute = t[4]
currentSecond = t[5]
# r is the number of seconds left between now and midnight
r = MIDNIGHT - ((currentHour * 60 + currentMinute) * 60 +
currentSecond)
result = currentTime + r
# If we are rolling over on a certain day, add in the number of days until
# the next rollover, but offset by 1 since we just calculated the time
# until the next day starts. There are three cases:
# Case 1) The day to rollover is today; in this case, do nothing
# Case 2) The day to rollover is further in the interval (i.e., today is
# day 2 (Wednesday) and rollover is on day 6 (Sunday). Days to
# next rollover is simply 6 - 2 - 1, or 3.
# Case 3) The day to rollover is behind us in the interval (i.e., today
# is day 5 (Saturday) and rollover is on day 3 (Thursday).
# Days to rollover is 6 - 5 + 3, or 4. In this case, it's the
# number of days left in the current week (1) plus the number
# of days in the next week until the rollover day (3).
# The calculations described in 2) and 3) above need to have a day added.
# This is because the above time calculation takes us to midnight on this
# day, i.e. the start of the next day.
if self.when.startswith('W'):
day = t[6] # 0 is Monday
if day != self.dayOfWeek:
if day < self.dayOfWeek:
daysToWait = self.dayOfWeek - day
else:
daysToWait = 6 - day + self.dayOfWeek + 1
newRolloverAt = result + (daysToWait * (60 * 60 * 24))
if not self.utc:
dstNow = t[-1]
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
result = newRolloverAt
return result
def shouldRollover(self, binrecord, textrecord):
"""
Determine if rollover should occur.
Just compare times.
"""
# time rolling first
t = int(time.time())
if t >= self.rolloverAt:
return RollingTypes.time_rolling
self.open()
if self.maxBytes > 0: # are we rolling over?
if self.textfile.tell() + len(textrecord) >= self.maxBytes:
return RollingTypes.size_rolling
if self.binfile.tell() + len(binrecord) >= self.maxBytes:
return RollingTypes.size_rolling
return RollingTypes.no_rolling
def getFilesToDelete(self):
"""
Determine the files to delete when rolling over.
"""
dirName, baseName = os.path.split(self.textpath)
files = []
prefix = baseName + "."
filePaths = glob.glob(os.path.join(dirName, "%s*" % prefix))
fileNames = [os.path.split(f)[1] for f in filePaths]
plen = len(prefix)
t_set = set()
for fileName in fileNames:
suffix = fileName[plen:]
if self.extMatch.match(suffix):
s = suffix.split(".")
t = s[1] if suffix.startswith("cbl") else s[0]
t_set.add(t)
files.append({'time': t, 'file': os.path.join(dirName,
fileName)})
t_list = list(t_set)
t_list.sort()
result = [f['file'] for f in files if
f['time'] in t_list[:-(self.backupCount - 1)]]
return result
def initSizeRollingCount(self):
"""
Init the max number of log files for current time.
"""
dirName, baseName = os.path.split(self.textpath)
prefix = baseName + "."
filePaths = glob.glob(os.path.join(dirName, "%s*" % prefix))
fileNames = [os.path.split(f)[1] for f in filePaths]
plen = len(prefix)
for fileName in fileNames:
suffix = fileName[plen:]
try:
self.sizeRollingCount = max(self.sizeRollingCount, int(suffix))
except ValueError:
pass
def _sizeRoll(self):
self.close()
for i in range(self.sizeRollingCount, 0, -1):
sbfn = "%s.%d" % (self.binpath, i)
dbfn = "%s.%d" % (self.binpath, i + 1)
stfn = "%s.%d" % (self.textpath, i)
dtfn = "%s.%d" % (self.textpath, i + 1)
if os.path.exists(sbfn):
if os.path.exists(dbfn):
os.remove(dbfn)
os.rename(sbfn, dbfn)
if os.path.exists(stfn):
if os.path.exists(dtfn):
os.remove(dtfn)
os.rename(stfn, dtfn)
# size rolling happens, add statistics count
self.sizeRollingCount += 1
dbfn = self.binpath + ".1"
dtfn = self.textpath + ".1"
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
if os.path.exists(self.binpath):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
os.rename(self.textpath, dtfn)
return dbfn, dtfn
def _timeRoll(self):
self.close()
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
# if size rolling files exist
for i in range(self.sizeRollingCount, 0, -1):
sbfn = "%s.%d" % ( self.binpath, i)
dbfn = "%s.%s.%d" % (
self.binpath, time.strftime(self.suffix, timeTuple),i)
stfn = "%s.%d" % (self.textpath, i)
dtfn = "%s.%s.%d" % (
self.textpath, time.strftime(self.suffix, timeTuple), i)
if os.path.exists(sbfn):
if os.path.exists(dbfn):
os.remove(dbfn)
os.rename(sbfn, dbfn)
if os.path.exists(stfn):
if os.path.exists(dtfn):
os.remove(dtfn)
os.rename(stfn, dtfn)
# As time rolling happens, reset statistics count
self.sizeRollingCount = 0
dbfn = self.binpath + "." + time.strftime(self.suffix, timeTuple)
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
if os.path.exists(self.binpath):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
os.rename(self.textpath, dtfn)
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
#If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt
return dbfn, dtfn
def doRollover(self, rolling_type):
"""
do a rollover based on the rolling type.
"""
if rolling_type == RollingTypes.size_rolling:
return self._sizeRoll()
if rolling_type == RollingTypes.time_rolling:
return self._timeRoll()
class Logger(object):
"""
:param console: If true, [] will be used to denote non-text events. If
@@ -122,26 +507,28 @@ class Logger(object):
self.initialized = True
self.filepath = confluent.config.configmanager.get_global("logdirectory")
if self.filepath is None:
self.filepath = "/var/log/confluent/"
if os.name == 'nt':
self.filepath = os.path.join(
os.getenv('SystemDrive'), '\\ProgramData', 'confluent',
'logs')
else:
self.filepath = "/var/log/confluent"
self.isconsole = console
if console:
self.filepath += "consoles/"
self.filepath = os.path.join(self.filepath, "consoles")
if not os.path.isdir(self.filepath):
os.makedirs(self.filepath, 448)
self.textpath = self.filepath + logname
self.binpath = self.filepath + logname + ".cbl"
self.writer = None
self.closer = None
self.textfile = None
self.binfile = None
self.handler = TimedAndSizeRotatingFileHandler(self.filepath, logname,
interval=1)
self.lockfile = None
self.logname = logname
self.logentries = collections.deque()
def writedata(self):
if self.textfile is None:
self.textfile = open(self.textpath, mode='ab')
if self.binfile is None:
self.binfile = open(self.binpath, mode='ab')
while self.logentries:
textfile, binfile = self.handler.open()
entry = self.logentries.popleft()
ltype = entry[0]
tstamp = entry[1]
@@ -156,8 +543,8 @@ class Logger(object):
elif not self.isconsole:
textdate = time.strftime(
'%b %d %H:%M:%S ', time.localtime(tstamp))
fcntl.flock(self.textfile, fcntl.LOCK_EX)
offset = self.textfile.tell() + len(textdate)
flock(textfile, LOCK_EX)
offset = textfile.tell() + len(textdate)
datalen = len(data)
eventaux = entry[4]
if eventaux is None:
@@ -175,53 +562,103 @@ class Logger(object):
textrecord = textdate + data
if not textrecord.endswith('\n'):
textrecord += '\n'
self.textfile.write(textrecord)
fcntl.flock(self.textfile, fcntl.LOCK_UN)
fcntl.flock(self.binfile, fcntl.LOCK_EX)
self.binfile.write(binrecord)
fcntl.flock(self.binfile, fcntl.LOCK_UN)
self.textfile.flush()
self.binfile.flush()
files = self.handler.try_emit(binrecord, textrecord)
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.
to_bfile, to_tfile = files
self.logentries.appendleft(entry)
roll_data = json.dumps({'previouslogfile': to_tfile})
self.logentries.appendleft([DataTypes.event, tstamp, roll_data,
Events.logrollover, None])
if self.closer is None:
self.closer = eventlet.spawn_after(15, self.closelog)
self.writer = None
def read_recent_text(self, size):
def parse_last_rolling_files(textfile, offset, datalen):
textfile.seek(offset, 0)
textpath = json.loads(textfile.read(datalen))['previouslogfile']
dir_name, base_name = os.path.split(textpath)
temp = base_name.split('.')
temp.insert(1,'cbl')
# find the recent bin file
binpath = os.path.join(dir_name, ".".join(temp))
return textpath, binpath
textpath = self.handler.textpath
binpath = self.handler.binpath
try:
textfile = open(self.textpath, mode='r')
binfile = open(self.binpath, mode='r')
textfile = open(textpath, mode='r')
binfile = open(binpath, mode='r')
except IOError:
return '', 0, 0
fcntl.flock(binfile, fcntl.LOCK_SH)
flock(binfile, LOCK_SH)
binfile.seek(0, 2)
binidx = binfile.tell() - 16
binidx = binfile.tell()
currsize = 0
offsets = []
termstate = None
recenttimestamp = 0
access_last_rename = False
flock(textfile, LOCK_SH)
while binidx > 0 and currsize < size:
binfile.seek(binidx, 0)
binidx -= 16
binfile.seek(binidx, 0)
recbytes = binfile.read(16)
(_, ltype, offset, datalen, tstamp, evtdata, eventaux, _) = \
struct.unpack(">BBIHIBBH", recbytes)
if ltype != 2:
# rolling events found.
if ltype == DataTypes.event and evtdata == Events.logrollover:
# Now, we can only find the last renamed file which logging
# the data.
if access_last_rename == False:
access_last_rename = True
else:
break
textpath, binpath = parse_last_rolling_files(textfile, offset,
datalen)
# Rolling event detected, close the current bin file, then open
# the renamed bin file.
flock(binfile, LOCK_UN)
binfile.close()
try:
binfile = open(binpath, mode='r')
except IOError:
return '', 0, 0
flock(binfile, LOCK_SH)
binfile.seek(0, 2)
binidx = binfile.tell()
# things have been set up for next iteration to dig to
# previous log file, go to next iteration
continue
elif ltype != 2:
continue
if tstamp > recenttimestamp:
recenttimestamp = tstamp
currsize += datalen
offsets.append((offset, datalen))
offsets.append((offset, datalen, textpath))
if termstate is None:
termstate = eventaux
fcntl.flock(binfile, fcntl.LOCK_UN)
flock(binfile, LOCK_UN)
binfile.close()
textdata = ''
fcntl.flock(textfile, fcntl.LOCK_SH)
while offsets:
(offset, length) = offsets.pop()
(offset, length, textpath) = offsets.pop()
if textfile.name != textpath:
flock(textfile, LOCK_UN)
textfile.close()
try:
textfile = open(textpath)
except IOError:
return '', 0, 0
textfile.seek(offset, 0)
textdata += textfile.read(length)
fcntl.flock(textfile, fcntl.LOCK_UN)
flock(textfile, LOCK_UN)
textfile.close()
if termstate is None:
termstate = 0
@@ -270,8 +707,5 @@ class Logger(object):
self.writer = eventlet.spawn_after(2, self.writedata)
def closelog(self):
self.textfile.close()
self.binfile.close()
self.textfile = None
self.binfile = None
self.handler.close()
self.closer = None
+74
View File
@@ -0,0 +1,74 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Utility library for interesting lookups of nodes.
# Examples:
# looking up a node by a hardwaremanagement.manager address
# looking up a node by uuid (actually pretty straightforward
# looking up a node by mac address
# These are generally in the context of coming in from some unstructured
# direction (alerts, PXE attempt) and for now will only look at the null
# tenant (all baremetal tenants that are expected to receive alert/pxe
# service should have a null tenant and a tenant entry that correlates)
__author__ = 'jjohnson2'
import confluent.config.configmanager as configmanager
import itertools
import socket
manager_to_nodemap = {}
def node_by_manager(manager):
"""Lookup a node by manager
Search for a node according to a given network address.
Rather than do a simple equality, it uses getaddrinfo
to allow name or ip and different forms of ip. For
example, 'fe80::0001' will match 'fe80::01' and
'127.000.000.001' will match '127.0.0.1'
:param manager: The ip or resolvable name of the manager
:returns: The node name (if any)
"""
manageraddresses = []
for tmpaddr in socket.getaddrinfo(manager, None):
manageraddresses.append(tmpaddr[4][0])
cfm = configmanager.ConfigManager(None)
if manager in manager_to_nodemap:
# We have a stored hint as to the most probably correct answer
# put that node at the head of the list in hopes of reducing
# iterations for a lookup in a large environment
# However we don't trust the answer either, since
# reconfiguration could have changed it and this mapping
# is not hooked into getting updates
check_nodes = itertools.chain(
(manager_to_nodemap[manager],), cfm.list_nodes())
else:
check_nodes = cfm.list_nodes()
hmattribs = cfm.get_node_attributes(check_nodes,
('hardwaremanagement.manager',))
for node in hmattribs:
currhm = hmattribs[node]['hardwaremanagement.manager']['value']
if currhm in manageraddresses:
manager_to_nodemap[manager] = node
return node
for curraddr in socket.getaddrinfo(currhm, None):
curraddr = curraddr[4][0]
if curraddr in manageraddresses:
manager_to_nodemap[manager] = node
return node
+63 -8
View File
@@ -27,21 +27,36 @@
import atexit
import confluent.auth as auth
import confluent.config.conf as conf
import confluent.config.configmanager as configmanager
import confluent.consoleserver as consoleserver
import confluent.core as confluentcore
import confluent.httpapi as httpapi
import confluent.log as log
import confluent.sockapi as sockapi
try:
import confluent.sockapi as sockapi
except ImportError:
#On platforms without pwd, give up on the sockapi in general and be http
#only for now
pass
import eventlet
#import eventlet.backdoor as backdoor
import fcntl
havefcntl = True
try:
import fcntl
except ImportError:
havefcntl = False
#import multiprocessing
import sys
import os
import signal
import time
import traceback
def _daemonize():
if not 'fork' in os.__dict__:
return
thispid = os.fork()
if thispid > 0:
os.waitpid(thispid, 0)
@@ -106,19 +121,49 @@ def _checkpidfile():
def terminate(signalname, frame):
sys.exit(0)
def dumptrace(signalname, frame):
ht = open('/var/log/confluent/hangtraces', 'a')
ht.write('Dumping active trace on ' + time.strftime('%X %x\n'))
ht.write(''.join(traceback.format_stack(frame)))
ht.close()
def doexit():
if not havefcntl:
return
pidfile = open('/var/run/confluent/pid')
pid = pidfile.read()
if pid == str(os.getpid()):
os.remove('/var/run/confluent/pid')
def _initsecurity(config):
if config.has_option('security', 'externalcfgkey'):
keyfile = config.get('security', 'externalcfgkey')
with open(keyfile, 'r') as keyhandle:
key = keyhandle.read()
configmanager.init_masterkey(key)
def run():
_checkpidfile()
confluentcore.load_plugins()
signal.signal(signal.SIGUSR1, dumptrace)
if havefcntl:
_checkpidfile()
conf.init_config()
try:
config = conf.get_config()
_initsecurity(config)
except:
sys.stderr.write("Error unlocking credential store\n")
doexit()
sys.exit(1)
try:
confluentcore.load_plugins()
except:
doexit()
raise
_daemonize()
_updatepidfile()
if havefcntl:
_updatepidfile()
auth.init_auth()
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
@@ -127,11 +172,21 @@ def run():
#dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
# family=socket.AF_UNIX)
#eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
consoleserver.start_console_sessions()
webservice = httpapi.HttpApi()
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
webservice.start()
sockservice = sockapi.SockApi()
sockservice.start()
try:
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port)
sockservice.start()
except NameError:
pass
atexit.register(doexit)
while 1:
eventlet.sleep(100)
def _get_connector_config(session):
host = conf.get_option(session, 'bindhost')
port = conf.get_int_option(session, 'bindport')
return (host, port)
+586 -54
View File
@@ -21,6 +21,13 @@
import confluent.exceptions as exc
import json
valid_health_values = set([
'ok',
'warning',
'critical',
'failed',
'unknown',
])
def _htmlify_structure(indict):
ret = "<ul>"
@@ -33,8 +40,14 @@ def _htmlify_structure(indict):
ret += _htmlify_structure(indict[key])
elif isinstance(indict, list):
if len(indict) > 0:
if type(indict[0]) in (str, unicode):
ret += ",".join(indict)
if type(indict[0]) in (str, unicode, None):
nd = []
for datum in indict:
if datum is None:
nd.append('')
else:
nd.append(datum)
ret += ",".join(nd)
else:
for v in indict:
ret += _htmlify_structure(v)
@@ -75,7 +88,7 @@ class ConfluentMessage(object):
self.kvpairs = self.kvpairs[node]
def html(self, extension=''):
#this is used to facilitate the api explorer feature
# this is used to facilitate the api explorer feature
if not hasattr(self, 'stripped'):
self.stripped = False
if not hasattr(self, 'notnode'):
@@ -94,8 +107,31 @@ class ConfluentMessage(object):
for key in pairs.iterkeys():
val = pairs[key]
value = self.defaultvalue
valtype = self.defaulttype
if isinstance(val, dict) and 'type' in val:
valtype = val['type']
else:
valtype = self.defaulttype
notes = []
if isinstance(val, list):
snippet += key + ":"
if len(val) == 0 and not self.readonly:
snippet += ('<input type="{0}" name="{1}" value="" '
' "title="{2}">'
).format(valtype, key, self.desc)
for v in val:
if self.readonly:
snippet += _htmlify_structure(v)
else:
snippet += ('<input type="{0}" name="{1}" value="{2}" '
' "title="{3}">\r'
).format(valtype, key, v, self.desc)
if not self.readonly:
snippet += (
'<input type="{0}" name="{1}" value="" title="{2}">'
'<input type="checkbox" name="restexplorerhonorkey" '
'value="{1}">\r').format(valtype, key, self.desc)
return snippet
if val is not None and 'value' in val:
value = val['value']
if 'inheritedfrom' in val:
@@ -119,32 +155,13 @@ class ConfluentMessage(object):
if 'inheritedfrom' in val:
notes.append('Inherited from %s' % val['inheritedfrom'])
value = '********'
if isinstance(val, list):
snippet += key + ":"
if len(val) == 0 and not self.readonly:
snippet += ('<input type="{0}" name="{1}" value="" '
' "title="{2}">'
).format(valtype, key, self.desc)
for v in val:
if self.readonly:
snippet += _htmlify_structure(v)
else:
snippet += ('<input type="{0}" name="{1}" value="{2}" '
' "title="{3}">'
).format(valtype, key, v, self.desc)
if not self.readonly:
snippet += (
'<input type="{0}" name="{1}" value="" title="{2}">'
'<input type="checkbox" name="restexplorerhonorkey" '
'value="{1}">').format(valtype, key, self.desc)
return snippet
if self.readonly:
snippet += "{0}: {1}".format(key, value)
else:
snippet += (key + ":" +
'<input type="{0}" name="{1}" value="{2}" '
'title="{3}"><input type="checkbox" '
'name="restexplorerhonorkey" value="{1}">'
'name="restexplorerhonorkey" value="{1}"><br>\r'
).format(valtype, key, value, self.desc)
if len(notes) > 0:
snippet += '(' + ','.join(notes) + ')'
@@ -163,18 +180,18 @@ class ConfluentNodeError(object):
return self.node + ":" + self.error
def strip_node(self, node):
#NOTE(jbjohnso): For single node errors, raise exception to
#trigger what a developer of that medium would expect
# NOTE(jjohnson2): For single node errors, raise exception to
# trigger what a developer of that medium would expect
raise Exception(self.error)
class ConfluentTargetTimeout(ConfluentNodeError):
def __init__(self, node):
def __init__(self, node, errstr='timeout'):
self.node = node
self.error = 'timeout'
self.error = errstr
def strip_node(self, node):
raise exc.TargetEndpointUnreachable
raise exc.TargetEndpointUnreachable(self.error)
class ConfluentTargetNotFound(ConfluentNodeError):
@@ -185,6 +202,7 @@ class ConfluentTargetNotFound(ConfluentNodeError):
def strip_node(self, node):
raise exc.NotFoundException(self.error)
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
def __init__(self, node):
self.node = node
@@ -248,7 +266,6 @@ class LinkRelation(ConfluentMessage):
self.href = ''
self.rel = ''
def json(self):
"""Provide json_hal style representation of the relation.
@@ -293,19 +310,68 @@ class ChildCollection(LinkRelation):
extension)
def get_input_message(path, operation, inputdata, nodes=None):
def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
if path[0] == 'power' and path[1] == 'state' and operation != 'retrieve':
return InputPowerMessage(path, nodes, inputdata)
elif path[0] in ('attributes', 'users') and operation != 'retrieve':
return InputAttributes(path, inputdata, nodes)
elif path == ['boot', 'nextdevice'] and operation != 'retrieve':
return InputBootDevice(path, nodes, inputdata)
elif path == [ 'identify' ] and operation != 'retrieve':
elif (len(path) == 5 and
path[:4] == ['configuration', 'management_controller', 'alerts',
'destinations'] and operation != 'retrieve'):
return InputAlertDestination(path, nodes, inputdata, multinode)
elif path == ['identify'] and operation != 'retrieve':
return InputIdentifyMessage(path, nodes, inputdata)
elif path == ['events', 'hardware', 'decode']:
return InputAlertData(path, inputdata, nodes)
elif (path[:3] == ['configuration', 'management_controller', 'users'] and
operation not in ('retrieve', 'delete') and path[-1] != 'all'):
return InputCredential(path, inputdata, nodes)
elif (path[:3] == ['configuration', 'management_controller', 'reset']
and operation != 'retrieve'):
return InputBMCReset(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'management_controller', 'identifier']
and operation != 'retrieve'):
return InputMCI(path, nodes, inputdata)
elif (path[:4] == ['configuration', 'management_controller',
'net_interfaces', 'management'] and operation != 'retrieve'):
return InputNetworkConfiguration(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'management_controller', 'domain_name']
and operation != 'retrieve'):
return InputDomainName(path, nodes, inputdata)
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
'enabled'] and operation != 'retrieve'):
return InputNTPEnabled(path, nodes, inputdata)
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
'servers'] and operation != 'retrieve' and len(path) == 5):
return InputNTPServer(path, nodes, inputdata)
elif inputdata:
raise exc.InvalidArgumentException()
class InputAlertData(ConfluentMessage):
def __init__(self, path, inputdata, nodes=None):
self.alertparams = inputdata
# first migrate snmpv1 input to snmpv2 format
if 'specifictrap' in self.alertparams:
# If we have a 'specifictrap', convert to SNMPv2 per RFC 2576
# This way
enterprise = self.alertparams['enterprise']
specifictrap = self.alertparams['specifictrap']
self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = enterprise + '.0.' + \
str(specifictrap)
if '1.3.6.1.6.3.1.1.4.1.0' in self.alertparams:
self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = \
self.alertparams['1.3.6.1.6.3.1.1.4.1.0']
if '.1.3.6.1.6.3.1.1.4.1.0' not in self.alertparams:
raise exc.InvalidArgumentException('Missing SNMP Trap OID')
def get_alert(self, node=None):
return self.alertparams
class InputAttributes(ConfluentMessage):
def __init__(self, path, inputdata, nodes=None):
self.nodeattribs = {}
@@ -355,7 +421,7 @@ class InputAttributes(ConfluentMessage):
# as above, use format() to see if string follows
# expression, store value back in case of escapes
tv = nodeattr[attr].format()
nodeattr[attr] = tv
nodeattr[attr] = str(tv)
except (KeyError, IndexError):
# an expression string will error if format() done
# use that as cue to put it into config as an expr
@@ -363,6 +429,80 @@ class InputAttributes(ConfluentMessage):
return nodeattr
class InputCredential(ConfluentMessage):
valid_privilege_levels = set([
'callback',
'user',
'operator',
'administrator',
'proprietary',
'no_access',
])
valid_enabled_values = set([
'yes',
'no'
])
def __init__(self, path, inputdata, nodes=None):
self.credentials = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if len(path) == 4:
inputdata['uid'] = path[-1]
# if the operation is 'create' check if all fields are present
elif ('uid' not in inputdata or 'privilege_level' not in inputdata or
'username' not in inputdata or 'password' not in inputdata):
raise exc.InvalidArgumentException('all fields are required')
if 'uid' not in inputdata:
raise exc.InvalidArgumentException('uid is missing')
if (isinstance(inputdata['uid'], str) and
not inputdata['uid'].isdigit()):
raise exc.InvalidArgumentException('uid must be a number')
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 nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for node in nodes:
self.credentials[node] = inputdata
def get_attributes(self, node):
if node not in self.credentials:
return {}
credential = self.credentials[node]
for attr in credentials:
if type(credentials[attr]) in (str, unicode):
try:
# as above, use format() to see if string follows
# expression, store value back in case of escapes
tv = credential[attr].format()
credential[attr] = tv
except (KeyError, IndexError):
# an expression string will error if format() done
# use that as cue to put it into config as an expr
credential[attr] = {'expression': credential[attr]}
return credential
class ConfluentInputMessage(ConfluentMessage):
keyname = 'state'
@@ -372,7 +512,7 @@ class ConfluentInputMessage(ConfluentMessage):
if not inputdata:
raise exc.InvalidArgumentException('missing input data')
if self.keyname not in inputdata:
#assume we have nested information
# assume we have nested information
for key in nodes:
if key not in inputdata:
raise exc.InvalidArgumentException(key + ' not in request')
@@ -388,7 +528,8 @@ class ConfluentInputMessage(ConfluentMessage):
else: # we have a state argument not by node
datum = inputdata
if self.keyname not in datum:
raise exc.InvalidArgumentException('missing {0} argument'.format(self.keyname))
raise exc.InvalidArgumentException(
'missing {0} argument'.format(self.keyname))
elif datum[self.keyname] not in self.valid_values:
raise exc.InvalidArgumentException(datum[self.keyname] +
' is not one of ' +
@@ -412,12 +553,125 @@ class InputPowerMessage(ConfluentInputMessage):
'off',
'reset',
'boot',
'diag',
'shutdown',
])
def powerstate(self, node):
return self.inputbynode[node]
class InputBMCReset(ConfluentInputMessage):
valid_values = set([
'reset',
])
def state(self, node):
return self.inputbynode[node]
class InputMCI(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
self.inputbynode = {}
self.stripped = False
if not inputdata or 'identifier' not in inputdata:
raise exc.InvalidArgumentException('missing input data')
if len(inputdata['identifier']) > 64:
raise exc.InvalidArgumentException(
'identifier must be less than or = 64 chars')
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for node in nodes:
self.inputbynode[node] = inputdata
def mci(self, node):
return self.inputbynode[node]['identifier']
class InputNetworkConfiguration(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
self.inputbynode = {}
self.stripped = False
if not inputdata:
raise exc.InvalidArgumentException('missing input data')
if 'hw_addr' in inputdata:
raise exc.InvalidArgumentException('hw_addr is a read only field')
if 'ipv4_address' not in inputdata:
inputdata['ipv4_address'] = None
if 'ipv4_gateway' not in inputdata:
inputdata['ipv4_gateway'] = None
if 'ipv4_configuration' in inputdata:
if inputdata['ipv4_configuration'].lower() not in ['dhcp','static']:
raise exc.InvalidArgumentException(
'Unrecognized ipv4_configuration')
else:
inputdata['ipv4_configuration'] = None
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for node in nodes:
self.inputbynode[node] = inputdata
def netconfig(self, node):
return self.inputbynode[node]
class InputDomainName(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
self.inputbynode = {}
self.stripped = False
if not inputdata or 'domain_name' not in inputdata:
raise exc.InvalidArgumentException('missing input data')
if len(inputdata['domain_name']) > 256:
raise exc.InvalidArgumentException(
'identifier must be less than or = 256 chars')
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for node in nodes:
self.inputbynode[node] = inputdata['domain_name']
def domain_name(self, node):
return self.inputbynode[node]
class InputNTPServer(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
self.inputbynode = {}
self.stripped = False
if not inputdata or 'server' not in inputdata:
raise exc.InvalidArgumentException('missing input data')
if len(inputdata['server']) > 256:
raise exc.InvalidArgumentException(
'identifier must be less than or = 256 chars')
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for node in nodes:
self.inputbynode[node] = str(inputdata['server'])
def ntp_server(self, node):
return self.inputbynode[node]
class InputNTPEnabled(ConfluentInputMessage):
valid_values = set([
'True',
'False'
])
def ntp_enabled(self, node):
return self.inputbynode[node]
class BootDevice(ConfluentChoiceMessage):
valid_values = set([
'network',
@@ -436,7 +690,7 @@ class BootDevice(ConfluentChoiceMessage):
valid_paramset = {
'bootmode': valid_bootmodes,
}
def __init__(self, node, device, bootmode='unspecified'):
if device not in self.valid_values:
@@ -448,7 +702,7 @@ class BootDevice(ConfluentChoiceMessage):
self.kvpairs = {
node: {
'nextdevice': {'value': device},
'bootmode': {'value': bootmode },
'bootmode': {'value': bootmode},
}
}
@@ -514,10 +768,164 @@ class PowerState(ConfluentChoiceMessage):
'off',
'reset',
'boot',
'shutdown',
'diag',
])
keyname = 'state'
class BMCReset(ConfluentChoiceMessage):
valid_values = set([
'reset',
])
keyname = 'state'
class NTPEnabled(ConfluentChoiceMessage):
valid_values = set([
'True',
'False',
])
def __init__(self, node, enabled):
self.stripped = False
self.kvpairs = {
node: {
'state': {'value': str(enabled)},
}
}
class EventCollection(ConfluentMessage):
"""A collection of events
This conveys a representation of an iterable of events. The following
fields are supported:
id (some data giving the class of event without the specific data of the
event. For example, 'overtemp (1000 degrees celsius)' would have
the same 'id' as 'overtemp (200 degrees celsius)
component (specific name of the component this event references if any)
component_type (A description of the sort of device component is)
event (A text description of the event that occurred)
severity (The text 'ok', 'warning', 'critical', 'failed', or 'unknown')
timestamp (ISO 8601 compliant timestamp if available)
"""
readonly = True
def __init__(self, events=(), name=None):
eventdata = []
self.notnode = name is None
for event in events:
entry = {
'id': event.get('id', None),
'component': event.get('component', None),
'component_type': event.get('component_type', None),
'event': event.get('event', None),
'severity': event['severity'],
'timestamp': event.get('timestamp', None),
'record_id': event.get('record_id', None),
}
if event['severity'] not in valid_health_values:
raise exc.NotImplementedException(
'Invalid severity - ' + repr(event['severity']))
eventdata.append(entry)
if self.notnode:
self.kvpairs = {'events': eventdata}
else:
self.kvpairs = {name: {'events': eventdata}}
class User(ConfluentMessage):
def __init__(self, uid, username, privilege_level, name=None):
self.desc = 'foo'
self.stripped = False
self.notnode = name is None
kvpairs = {'username': {'value': username},
'password': {'value': '', 'type': 'password'},
'privilege_level': {'value': privilege_level},
'enabled': {'value': ''}
}
if self.notnode:
self.kvpairs = kvpairs
else:
self.kvpairs = {name: kvpairs}
class UserCollection(ConfluentMessage):
readonly = True
def __init__(self, users=(), name=None):
self.notnode = name is None
self.desc = 'list of users'
userlist = []
for user in users:
entry = {
'uid': user['uid'],
'username': user['name'],
'privilege_level': user['access']['privilege_level']
}
userlist.append(entry)
if self.notnode:
self.kvpairs = {'users': userlist}
else:
self.kvpairs = {name: {'users': userlist}}
class AlertDestination(ConfluentMessage):
def __init__(self, ip, acknowledge=False, acknowledge_timeout=None, retries=0, name=None):
self.desc = 'foo'
self.stripped = False
self.notnode = name is None
kvpairs = {'ip': {'value': ip},
'acknowledge': {'value': acknowledge},
'acknowledge_timeout': {'value': acknowledge_timeout},
'retries': {'value': retries}}
if self.notnode:
self.kvpairs = kvpairs
else:
self.kvpairs = {name: kvpairs}
class InputAlertDestination(ConfluentMessage):
valid_alert_params = {
'acknowledge': lambda x: False if type(x) in (unicode,str) 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)
}
def __init__(self, path, nodes, inputdata, multinode=False):
self.alertcfg = {}
if multinode: # keys are node names
for node in inputdata:
self.alertcfg[node] = inputdata[node]
for key in inputdata[node]:
if key not in self.valid_alert_params:
raise exc.InvalidArgumentException(
'Unrecognized alert parameter ' + key)
if isinstance(inputdata[node][key], dict):
self.alertcfg[node][key] = \
self.valid_alert_params[key](
inputdata[node][key]['value'])
else:
self.alertcfg[node][key] = \
self.valid_alert_params[key](inputdata[node][key])
else:
for key in inputdata:
if key not in self.valid_alert_params:
raise exc.InvalidArgumentException(
'Unrecognized alert parameter ' + key)
if isinstance(inputdata[key], dict):
inputdata[key] = self.valid_alert_params[key](
inputdata[key]['value'])
else:
inputdata[key] = self.valid_alert_params[key](
inputdata[key])
for node in nodes:
self.alertcfg[node] = inputdata
def alert_params_by_node(self, node):
return self.alertcfg[node]
class SensorReadings(ConfluentMessage):
@@ -527,15 +935,19 @@ class SensorReadings(ConfluentMessage):
readings = []
self.notnode = name is None
for sensor in sensors:
sensordict = {'name': sensor['name']}
if 'value' in sensor:
sensordict['value'] = sensor['value']
if 'units' in sensor:
sensordict['units'] = sensor['units']
if 'states' in sensor:
sensordict['states'] = sensor['states']
if 'health' in sensor:
sensordict['health'] = sensor['health']
sensordict = {'name': sensor.name}
if hasattr(sensor, 'value'):
sensordict['value'] = sensor.value
if hasattr(sensor, 'units'):
sensordict['units'] = sensor.units
if hasattr(sensor, 'states'):
sensordict['states'] = sensor.states
if hasattr(sensor, 'state_ids'):
sensordict['state_ids'] = sensor.state_ids
if hasattr(sensor, 'health'):
sensordict['health'] = sensor.health
if hasattr(sensor, 'type'):
sensordict['type'] = sensor.type
readings.append(sensordict)
if self.notnode:
self.kvpairs = {'sensors': readings}
@@ -543,14 +955,65 @@ class SensorReadings(ConfluentMessage):
self.kvpairs = {name: {'sensors': readings}}
class Firmware(ConfluentMessage):
readonly = True
def __init__(self, data, name):
self.notnode = name is None
self.desc = 'Firmware information'
if self.notnode:
self.kvpairs = {'firmware': data}
else:
self.kvpairs = {name: {'firmware': data}}
class KeyValueData(ConfluentMessage):
readonly = True
def __init__(self, kvdata, name=None):
self.notnode = name is None
if self.notnode:
self.kvpairs = kvdata
else:
self.kvpairs = {name: kvdata}
class LEDStatus(ConfluentMessage):
readonly = True
def __init__(self, data, name):
self.notnode = name is None
self.desc = 'led status'
if self.notnode:
self.kvpairs = {'leds':data}
else:
self.kvpairs = {name: {'leds':data}}
class NetworkConfiguration(ConfluentMessage):
desc = 'Network configuration'
def __init__(self, name=None, ipv4addr=None, ipv4gateway=None,
ipv4cfgmethod=None, hwaddr=None):
self.notnode = name is None
self.stripped = False
kvpairs = {
'ipv4_address': {'value': ipv4addr},
'ipv4_gateway': {'value': ipv4gateway},
'ipv4_configuration': {'value': ipv4cfgmethod},
'hw_addr': {'value': hwaddr},
}
if self.notnode:
self.kvpairs = kvpairs
else:
self.kvpairs = {name: kvpairs}
class HealthSummary(ConfluentMessage):
readonly = True
valid_values = set([
'ok',
'warning',
'critical',
'failed',
])
valid_values = valid_health_values
def __init__(self, health, name=None):
self.stripped = False
@@ -588,7 +1051,76 @@ class ListAttributes(ConfluentMessage):
if self.notnode:
self.kvpairs = kv
else:
self.kvpairs = {node: kv}
self.kvpairs = {name: kv}
class MCI(ConfluentMessage):
def __init__(self, name=None, mci=None):
self.notnode = name is None
self.desc = 'BMC identifier'
kv = {'identifier': {'value': mci}}
if self.notnode:
self.kvpairs = kv
else:
self.kvpairs = {name: kv}
class DomainName(ConfluentMessage):
def __init__(self, name=None, dn=None):
self.notnode = name is None
self.desc = 'BMC domain name'
kv = {'domain_name': {'value': dn}}
if self.notnode:
self.kvpairs = kv
else:
self.kvpairs = {name: kv}
class NTPServers(ConfluentMessage):
readonly = True
def __init__(self, name=None, servers=None):
self.notnode = name is None
self.desc = 'NTP Server'
kv = []
for idx in range(0, len(servers)):
kv.append({str(idx+1): servers[idx]})
if self.notnode:
self.kvpairs = {'ntp_servers': kv}
else:
self.kvpairs = {name: {'ntp_servers': kv}}
class NTPServer(ConfluentMessage):
def __init__(self, name=None, server=None):
self.notnode = name is None
self.desc = 'NTP Server'
kv = {
'server': {'value': server},
}
if self.notnode:
self.kvpairs = kv
else:
self.kvpairs = {name: kv}
class License(ConfluentMessage):
readonly = True
def __init__(self, name=None, kvm=None):
self.notnode = name is None
self.desc = 'License'
kv = []
kv.append({'kvm_availability': str(kvm)})
if self.notnode:
self.kvpairs = {'License': kv}
else:
self.kvpairs = {name: {'License': kv}}
class CryptedAttributes(Attributes):
@@ -611,5 +1143,5 @@ class CryptedAttributes(Attributes):
self.kvpairs = nkv
else:
self.kvpairs = {
node: nkv
name: nkv
}
+4 -3
View File
@@ -20,12 +20,13 @@
# the middle of strings and use of @ for anything is not in their syntax
import copy
import itertools
import pyparsing as pp
import re
# construct custom grammar with pyparsing
_nodeword = pp.Word(pp.alphanums + '~^$/=-:.*+!')
_nodeword = pp.Word(pp.alphanums + '~^$/=-_:.*+!')
_nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']',
unquoteResults=False)
_nodeatom = pp.Group(pp.OneOrMore(_nodeword | _nodebracket))
@@ -160,7 +161,7 @@ class NodeRange(object):
return set([entname])
if self.cfm.is_nodegroup(entname):
grpcfg = self.cfm.get_nodegroup_attributes(entname)
nodes = grpcfg['nodes']
nodes = copy.copy(grpcfg['nodes'])
if 'noderange' in grpcfg and grpcfg['noderange']:
nodes |= NodeRange(
grpcfg['noderange']['value'], self.cfm).nodes
@@ -185,7 +186,7 @@ class NodeRange(object):
return set([element])
if self.cfm.is_nodegroup(element):
grpcfg = self.cfm.get_nodegroup_attributes(element)
nodes = grpcfg['nodes']
nodes = copy.copy(grpcfg['nodes'])
if 'noderange' in grpcfg and grpcfg['noderange']:
nodes |= NodeRange(
grpcfg['noderange']['value'], self.cfm).nodes
@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import atexit
import confluent.exceptions as exc
import confluent.interface.console as conapi
import confluent.messages as msg
@@ -30,6 +31,12 @@ import socket
console.session.select = eventlet.green.select
console.session.threading = eventlet.green.threading
def exithandler():
console.session.iothread.join()
atexit.register(exithandler)
_ipmiworkers = greenpool.GreenPool()
_ipmithread = None
@@ -41,9 +48,43 @@ sensor_categories = {
'fans': frozenset(['Fan', 'Cooling Device']),
}
def hex2bin(hexstring):
hexvals = hexstring.split(':')
if len(hexvals) < 2:
hexvals = hexstring.split(' ')
if len(hexvals) < 2:
hexvals = [hexstring[i:i+2] for i in xrange(0, len(hexstring), 2)]
bytedata = [int(i, 16) for i in hexvals]
return bytearray(bytedata)
def simplify_name(name):
return name.lower().replace(' ', '_')
return name.lower().replace(' ', '_').replace('/', '-')
def sanitize_invdata(indata):
"""Sanitize pyghmi data
pyghmi will return bytearrays when it has no idea what to do. In our
case, we will change those to hex strings. Additionally, ignore 'extra'
fields if the oem_parser is set
"""
if 'oem_parser' in indata and indata['oem_parser'] is not None:
if 'board_extra' in indata:
del indata['board_extra']
if 'chassis_extra' in indata:
del indata['chassis_extra']
if 'product_extra' in indata:
del indata['product_extra']
for k in indata:
if isinstance(indata[k], bytearray):
indata[k] = '0x' + ''.join(format(x, '02x') for x in indata[k])
elif isinstance(indata[k], dict):
sanitize_invdata(indata[k])
elif isinstance(indata[k], list):
for idx, value in enumerate(indata[k]):
if isinstance(value, bytearray):
indata[k][idx] = '0x' + ''.join(
format(x, '02x') for x in indata[k][idx])
class IpmiCommandWrapper(ipmicommand.Command):
@@ -223,22 +264,22 @@ def perform_request(operator, node, element,
results.put(msg.ConfluentTargetTimeout(node))
else:
results.put(msg.ConfluentNodeError(node, excmsg))
raise
except exc.TargetEndpointUnreachable as tu:
results.put(msg.ConfluentTargetTimeout(node, str(tu)))
except Exception as e:
results.put(msg.ConfluentNodeError(
node, 'IPMI PluginException (see stderr log): ' + str(e)))
raise
finally:
results.put('Done')
persistent_ipmicmds = {}
def _dict_sensor(pygreading):
retdict = {'name': pygreading.name, 'value': pygreading.value,
'states': pygreading.states, 'units': pygreading.units,
'health': _str_health(pygreading.health)}
return retdict
class IpmiHandler(object):
def __init__(self, operation, node, element, cfd, inputdata, cfg, output):
self.sensormap = {}
self.invmap = {}
self.output = output
self.sensorcategory = None
self.broken = False
@@ -285,12 +326,17 @@ class IpmiHandler(object):
self._logevt.wait()
self._logevt = None
if self.broken:
if self.error == 'timeout':
self.output.put(msg.ConfluentTargetTimeout(self.node))
if (self.error == 'timeout' or
'Insufficient resources' in self.error):
self.error = self.error.replace(' reported in RAKP4', '')
self.output.put(msg.ConfluentTargetTimeout(
self.node, self.error))
return
elif ('Unauthorized' in self.error or
'Incorrect password' in self.error):
self.output.put(
msg.ConfluentTargetInvalidCredentials(self.node))
return
else:
raise Exception(self.error)
if self.element == ['power', 'state']:
@@ -303,6 +349,190 @@ class IpmiHandler(object):
self.identify()
elif self.element[0] == 'sensors':
self.handle_sensors()
elif self.element[0] == 'configuration':
self.handle_configuration()
elif self.element[0] == 'inventory':
self.handle_inventory()
elif self.element == ['events', 'hardware', 'log']:
self.do_eventlog()
elif self.element == ['events', 'hardware', 'decode']:
self.decode_alert()
elif self.element == ['console', 'license']:
self.handle_license()
else:
raise Exception('Not Implemented')
def handle_configuration(self):
if self.element[1:3] == ['management_controller', 'alerts']:
return self.handle_alerts()
elif self.element[1:3] == ['management_controller', 'users']:
return self.handle_users()
elif self.element[1:3] == ['management_controller', 'net_interfaces']:
return self.handle_nets()
elif self.element[1:3] == ['management_controller', 'reset']:
return self.handle_reset()
elif self.element[1:3] == ['management_controller', 'identifier']:
return self.handle_identifier()
elif self.element[1:3] == ['management_controller', 'domain_name']:
return self.handle_domain_name()
elif self.element[1:3] == ['management_controller', 'ntp']:
return self.handle_ntp()
raise Exception('Not implemented')
def decode_alert(self):
inputdata = self.inputdata.get_alert(self.node)
specifictrap = int(inputdata['.1.3.6.1.6.3.1.1.4.1.0'].rpartition(
'.')[-1])
for tmpvarbind in inputdata:
if tmpvarbind.endswith('3183.1.1'):
varbinddata = inputdata[tmpvarbind]
varbinddata = hex2bin(varbinddata)
event = self.ipmicmd.decode_pet(specifictrap, varbinddata)
self.pyghmi_event_to_confluent(event)
self.output.put(msg.EventCollection((event,), name=self.node))
def handle_alerts(self):
if self.element[3] == 'destinations':
if len(self.element) == 4:
# A list of destinations
maxdest = self.ipmicmd.get_alert_destination_count()
for alertidx in xrange(0, maxdest + 1):
self.output.put(msg.ChildCollection(alertidx))
return
elif len(self.element) == 5:
alertidx = int(self.element[-1])
if self.op == 'read':
destdata = self.ipmicmd.get_alert_destination(alertidx)
self.output.put(msg.AlertDestination(
ip=destdata['address'],
acknowledge=destdata['acknowledge_required'],
acknowledge_timeout=destdata.get('acknowledge_timeout', None),
retries=destdata['retries'],
name=self.node))
return
elif self.op == 'update':
alertparms = self.inputdata.alert_params_by_node(
self.node)
alertargs = {}
if 'acknowledge' in alertparms:
alertargs['acknowledge_required'] = alertparms['acknowledge']
if 'acknowledge_timeout' in alertparms:
alertargs['acknowledge_timeout'] = alertparms['acknowledge_timeout']
if 'ip' in alertparms:
alertargs['ip'] = alertparms['ip']
if 'retries' in alertparms:
alertargs['retries'] = alertparms['retries']
self.ipmicmd.set_alert_destination(destination=alertidx,
**alertargs)
return
elif self.op == 'delete':
self.ipmicmd.clear_alert_destination(alertidx)
return
raise Exception('Not implemented')
def handle_nets(self):
if len(self.element) == 3:
if self.op != 'read':
self.output.put(
msg.ConfluentNodeError(self.node, 'Unsupported operation'))
return
self.output.put(msg.ChildCollection('management'))
elif len(self.element) == 4 and self.element[-1] == 'management':
if self.op == 'read':
lancfg = self.ipmicmd.get_net_configuration()
self.output.put(msg.NetworkConfiguration(
self.node, ipv4addr=lancfg['ipv4_address'],
ipv4gateway=lancfg['ipv4_gateway'],
ipv4cfgmethod=lancfg['ipv4_configuration'],
hwaddr=lancfg['mac_address']
))
elif self.op == 'update':
config = self.inputdata.netconfig(self.node)
self.ipmicmd.set_net_configuration(
ipv4_address=config['ipv4_address'],
ipv4_configuration=config['ipv4_configuration'],
ipv4_gateway=config['ipv4_gateway'])
def handle_users(self):
# Create user
if len(self.element) == 3:
if self.op == 'update':
user = self.inputdata.credentials[self.node]
self.ipmicmd.create_user(uid=user['uid'], name=user['username'],
password=user['password'],
callback=True,link_auth=True, ipmi_msg=True,
privilege_level=user['privilege_level'])
# A list of users
self.output.put(msg.ChildCollection('all'))
for user in self.ipmicmd.get_users():
self.output.put(msg.ChildCollection(user, candelete=True))
return
# List all users
elif len(self.element) == 4 and self.element[-1] == 'all':
users = []
for user in self.ipmicmd.get_users():
users.append(self.ipmicmd.get_user(uid=user))
self.output.put(msg.UserCollection(users=users, name=self.node))
return
# Update user
elif len(self.element) == 4:
user = int(self.element[-1])
if self.op == 'read':
data = self.ipmicmd.get_user(uid=user)
self.output.put(msg.User(
uid=data['uid'],
username=data['name'],
privilege_level=data['access']['privilege_level'],
name=self.node))
return
elif self.op == 'update':
user = self.inputdata.credentials[self.node]
if 'username' in user:
self.ipmicmd.set_user_name(uid=user['uid'],
name=user['username'])
if 'privilege_level' in user:
self.ipmicmd.set_user_access(uid=user['uid'],
privilege_level=user['privilege_level'])
if 'password' in user:
self.ipmicmd.set_user_password(uid=user['uid'],
password=user['password'])
self.ipmicmd.set_user_password(uid=user['uid'],
mode='enable', password=user['password'])
if 'enabled' in user:
if user['enabled'] == 'yes':
mode = 'enable'
else:
mode = 'disable'
self.ipmicmd.disable_user(user['uid'], mode)
return
elif self.op == 'delete':
self.ipmicmd.user_delete(uid=user)
return
def do_eventlog(self):
eventout = []
clear = False
if self.op == 'delete':
clear = True
for event in self.ipmicmd.get_event_log(clear):
self.pyghmi_event_to_confluent(event)
eventout.append(event)
self.output.put(msg.EventCollection(eventout, name=self.node))
def pyghmi_event_to_confluent(self, event):
event['severity'] = _str_health(event.get('severity', 'unknown'))
if 'event_data' in event:
event['event'] = '{0} - {1}'.format(
event['event'], event['event_data'])
if 'event_id' in event:
event['id'] = '{0}.{1}'.format(event['event_id'],
event['component_type_id'])
def make_inventory_map(self):
invnames = self.ipmicmd.get_inventory_descriptions()
for name in invnames:
self.invmap[simplify_name(name)] = name
def make_sensor_map(self, sensors=None):
if sensors is None:
@@ -324,7 +554,9 @@ class IpmiHandler(object):
if ie.ipmicode == 203:
continue
raise
readings.append(_dict_sensor(reading))
if hasattr(reading, 'health'):
reading.health = _str_health(reading.health)
readings.append(reading)
self.output.put(msg.SensorReadings(readings, name=self.node))
else:
self.make_sensor_map()
@@ -335,21 +567,103 @@ class IpmiHandler(object):
return
reading = self.ipmicmd.get_sensor_reading(
self.sensormap[sensorname])
if hasattr(reading, 'health'):
reading.health = _str_health(reading.health)
self.output.put(
msg.SensorReadings([_dict_sensor(reading)],
msg.SensorReadings([reading],
name=self.node))
except pygexc.IpmiException:
self.output.put(msg.ConfluentTargetTimeout(self.node))
def list_inventory(self):
try:
components = self.ipmicmd.get_inventory_descriptions()
except pygexc.IpmiException:
self.output.put(msg.ConfluentTargetTimeout(self.node))
return
self.output.put(msg.ChildCollection('all'))
for component in components:
self.output.put(msg.ChildCollection(simplify_name(component)))
def list_firmware(self):
self.output.put(msg.ChildCollection('all'))
for id, data in self.ipmicmd.get_firmware():
self.output.put(msg.ChildCollection(simplify_name(id)))
def read_firmware(self, component):
items = []
for id, data in self.ipmicmd.get_firmware():
if component == 'all' or component == simplify_name(id):
items.append({id: data})
self.output.put(msg.Firmware(items, self.node))
def handle_inventory(self):
if self.element[1] == 'firmware':
if len(self.element) == 3:
return self.list_firmware()
elif len(self.element) == 4:
return self.read_firmware(self.element[-1])
elif self.element[1] == 'hardware':
if len(self.element) == 3: # list things in inventory
return self.list_inventory()
elif len(self.element) == 4: # actually read inventory data
return self.read_inventory(self.element[-1])
raise Exception('Unsupported scenario...')
def list_leds(self):
self.output.put(msg.ChildCollection('all'))
for category, info in self.ipmicmd.get_leds():
self.output.put(msg.ChildCollection(simplify_name(category)))
def read_leds(self, component):
led_categories = []
for category, info in self.ipmicmd.get_leds():
if component == 'all' or component == simplify_name(category):
led_categories.append({category: info})
self.output.put(msg.LEDStatus(led_categories, self.node))
def read_inventory(self, component):
invitems = []
if component == 'all':
for invdata in self.ipmicmd.get_inventory():
if invdata[1] is None:
newinf = {'present': False, 'information': None}
else:
sanitize_invdata(invdata[1])
newinf = {'present': True, 'information': invdata[1]}
newinf['name'] = invdata[0]
invitems.append(newinf)
else:
self.make_inventory_map()
compname = self.invmap.get(component, None)
if compname is None:
self.output.put(msg.ConfluentTargetNotFound())
return
invdata = self.ipmicmd.get_inventory_of_component(compname)
if invdata is None:
newinf = {'present': False, 'information': None}
else:
sanitize_invdata(invdata)
newinf = {'present': True, 'information': invdata}
newinf['name'] = compname
invitems.append(newinf)
newinvdata = {'inventory': invitems}
self.output.put(msg.KeyValueData(newinvdata, self.node))
def handle_sensors(self):
if self.element[-1] == '':
self.element = self.element[:-1]
if len(self.element) < 3:
return
self.sensorcategory = self.element[2]
if len(self.element) == 3: # list sensors per category
# list sensors per category
if len(self.element) == 3 and self.element[-2] == 'hardware':
if self.sensorcategory == 'leds':
return self.list_leds()
return self.list_sensors()
elif len(self.element) == 4: # resource requested
if self.sensorcategory == 'leds':
return self.read_leds(self.element[-1])
return self.read_sensors(self.element[-1])
def match_sensor(self, sensor):
@@ -382,7 +696,9 @@ class IpmiHandler(object):
if 'badreadings' in response:
badsensors = []
for reading in response['badreadings']:
badsensors.append(_dict_sensor(reading))
if hasattr(reading, 'health'):
reading.health = _str_health(reading.health)
badsensors.append(reading)
self.output.put(msg.SensorReadings(badsensors, name=self.node))
else:
raise exc.InvalidArgumentException('health is read-only')
@@ -401,6 +717,7 @@ class IpmiHandler(object):
self.output.put(msg.BootDevice(node=self.node,
device=bootdev['bootdev'],
bootmode=bootmode))
return
elif 'update' == self.op:
bootdev = self.inputdata.bootdevice(self.node)
douefi = False
@@ -418,24 +735,103 @@ class IpmiHandler(object):
self.ipmicmd.set_identify(on=identifystate)
self.output.put(msg.IdentifyState(
node=self.node, state=self.inputdata.inputbynode[self.node]))
return
elif 'read' == self.op:
# ipmi has identify as read-only for now
self.output.put(msg.IdentifyState(node=self.node, state=''))
return
def power(self):
if 'read' == self.op:
power = self.ipmicmd.get_power()
self.output.put(msg.PowerState(node=self.node,
state=power['powerstate']))
return
elif 'update' == self.op:
powerstate = self.inputdata.powerstate(self.node)
self.ipmicmd.set_power(powerstate, wait=30)
power = self.ipmicmd.get_power()
self.output.put(msg.PowerState(node=self.node,
state=power['powerstate']))
return
def handle_reset(self):
if 'read' == self.op:
self.output.put(msg.BMCReset(node=self.node,
state='reset'))
return
elif 'update' == self.op:
self.ipmicmd.reset_bmc()
return
def handle_identifier(self):
if 'read' == self.op:
mci = self.ipmicmd.get_mci()
self.output.put(msg.MCI(self.node, mci))
return
elif 'update' == self.op:
mci = self.inputdata.mci(self.node)
self.ipmicmd.set_mci(mci)
return
def handle_domain_name(self):
if 'read' == self.op:
dn = self.ipmicmd.get_domain_name()
self.output.put(msg.DomainName(self.node, dn))
return
elif 'update' == self.op:
dn = self.inputdata.domain_name(self.node)
self.ipmicmd.set_domain_name(dn)
return
def handle_ntp(self):
if self.element[3] == 'enabled':
if 'read' == self.op:
enabled = self.ipmicmd.get_ntp_enabled()
self.output.put(msg.NTPEnabled(self.node, enabled))
return
elif 'update' == self.op:
enabled = self.inputdata.ntp_enabled(self.node)
self.ipmicmd.set_ntp_enabled(enabled == 'True')
return
elif self.element[3] == 'servers':
if len(self.element) == 4:
self.output.put(msg.ChildCollection('all'))
size = len(self.ipmicmd.get_ntp_servers())
for idx in range(1, size + 1):
self.output.put(msg.ChildCollection(idx))
else:
if 'read' == self.op:
if self.element[-1] == 'all':
servers = self.ipmicmd.get_ntp_servers()
self.output.put(msg.NTPServers(self.node, servers))
return
else:
idx = int(self.element[-1]) - 1
servers = self.ipmicmd.get_ntp_servers()
self.output.put(msg.NTPServer(self.node, servers[idx]))
return
elif self.op in ('update', 'create'):
if self.element[-1] == 'all':
servers = self.inputdata.ntp_servers(self.node)
for idx in servers:
self.ipmicmd.set_ntp_server(server[idx],
int(idx[-1])-1)
return
else:
idx = int(self.element[-1]) - 1
server = self.inputdata.ntp_server(self.node)
self.ipmicmd.set_ntp_server(server, idx)
return
def handle_license(self):
available = self.ipmicmd.get_remote_kvm_available()
self.output.put(msg.License(self.node, available))
return
def _str_health(health):
if health == 'unknown':
return health
if pygconstants.Health.Failed & health:
health = 'failed'
elif pygconstants.Health.Critical & health:
@@ -471,4 +867,9 @@ def update(nodes, element, configmanager, inputdata):
def retrieve(nodes, element, configmanager, inputdata):
initthread()
return perform_requests('read', nodes, element, configmanager, inputdata)
return perform_requests('read', nodes, element, configmanager, inputdata)
def delete(nodes, element, configmanager, inputdata):
initthread()
return perform_requests(
'delete', nodes, element, configmanager, inputdata)
+53 -34
View File
@@ -17,9 +17,10 @@
# This is the socket api layer.
# It implement unix and tls sockets
#
#
import atexit
import errno
import os
import pwd
import stat
@@ -62,15 +63,23 @@ class ClientConsole(object):
if not self.xmit:
self.pendingdata.append(data)
return
tlvdata.send(self.client, data)
send_data(self.client, data)
def startsending(self):
self.xmit = True
for datum in self.pendingdata:
tlvdata.send(self.client, datum)
send_data(self.client, datum)
self.pendingdata = None
def send_data(connection, data):
try:
tlvdata.send(connection, data)
except IOError as ie:
if ie.errno != errno.EPIPE:
raise
def sessionhdl(connection, authname, skipauth=False):
# For now, trying to test the console stuff, so let's just do n4.
authenticated = False
@@ -84,9 +93,9 @@ def sessionhdl(connection, authname, skipauth=False):
if authdata is not None:
cfm = authdata[1]
authenticated = True
tlvdata.send(connection, "Confluent -- v0 --")
send_data(connection, "Confluent -- v0 --")
while not authenticated: # prompt for name and passphrase
tlvdata.send(connection, {'authpassed': 0})
send_data(connection, {'authpassed': 0})
response = tlvdata.recv(connection)
authname = response['username']
passphrase = response['password']
@@ -101,44 +110,49 @@ def sessionhdl(connection, authname, skipauth=False):
else:
authenticated = True
cfm = authdata[1]
tlvdata.send(connection, {'authpassed': 1})
send_data(connection, {'authpassed': 1})
request = tlvdata.recv(connection)
while request is not None:
try:
process_request(
connection, request, cfm, authdata, authname, skipauth)
except exc.ForbiddenRequest:
tlvdata.send(connection, {'errorcode': 403,
send_data(connection, {'errorcode': 403,
'error': 'Forbidden'})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
except exc.TargetEndpointBadCredentials:
tlvdata.send(connection, {'errorcode': 502,
send_data(connection, {'errorcode': 502,
'error': 'Bad Credentials'})
tlvdata.send(connection, {'_requestdone': 1})
except exc.TargetEndpointUnreachable:
tlvdata.send(connection, {'errorcode': 504,
'error': 'Unreachable Target'})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
except exc.TargetEndpointUnreachable as tu:
send_data(connection, {'errorcode': 504,
'error': 'Unreachable Target - ' + str(
tu)})
send_data(connection, {'_requestdone': 1})
except exc.NotImplementedException:
tlvdata.send(connection, {'errorcode': 501,
send_data(connection, {'errorcode': 501,
'error': 'Not Implemented'})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
except exc.NotFoundException as nfe:
tlvdata.send(connection, {'errorcode': 404,
send_data(connection, {'errorcode': 404,
'error': str(nfe)})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
except exc.InvalidArgumentException as iae:
tlvdata.send(connection, {'errorcode': 400,
send_data(connection, {'errorcode': 400,
'error': 'Bad Request - ' + str(iae)})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
except exc.LockedCredentials as lockedcred:
send_data(connection, {'errorcode': 500,
'error': 'Locked Credential Store'})
send_data(connection, {'_requestdone': 1})
except SystemExit:
sys.exit(0)
except:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
tlvdata.send(connection, {'errorcode': 500,
send_data(connection, {'errorcode': 500,
'error': 'Unexpected error'})
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, {'_requestdone': 1})
request = tlvdata.recv(connection)
@@ -146,8 +160,8 @@ def send_response(responses, connection):
if responses is None:
return
for rsp in responses:
tlvdata.send(connection, rsp.raw())
tlvdata.send(connection, {'_requestdone': 1})
send_data(connection, rsp.raw())
send_data(connection, {'_requestdone': 1})
def process_request(connection, request, cfm, authdata, authname, skipauth):
@@ -187,11 +201,11 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
datacallback=ccons.sendall, skipreplay=skipreplay)
if consession is None:
raise Exception("TODO")
tlvdata.send(connection, {'started': 1})
send_data(connection, {'started': 1})
ccons.startsending()
bufferage = consession.get_buffer_age()
if bufferage is not False:
tlvdata.send(connection, {'bufferage': bufferage})
send_data(connection, {'bufferage': bufferage})
while consession is not None:
data = tlvdata.recv(connection)
if type(data) == dict:
@@ -215,22 +229,22 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
else:
hdlr = pluginapi.handle_path(path, operation, cfm, params)
except exc.NotFoundException as e:
tlvdata.send(connection, {"errorcode": 404,
send_data(connection, {"errorcode": 404,
"error": "Target not found - " + str(e)})
tlvdata.send(connection, {"_requestdone": 1})
send_data(connection, {"_requestdone": 1})
except exc.InvalidArgumentException as e:
tlvdata.send(connection, {"errorcode": 400,
send_data(connection, {"errorcode": 400,
"error": "Bad Request - " + str(e)})
tlvdata.send(connection, {"_requestdone": 1})
send_data(connection, {"_requestdone": 1})
send_response(hdlr, connection)
return
def _tlshandler():
def _tlshandler(bind_host, bind_port):
plainsocket = socket.socket(socket.AF_INET6)
plainsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
plainsocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
plainsocket.bind(('::', 13001, 0, 0))
plainsocket.bind((bind_host, bind_port, 0, 0))
plainsocket.listen(5)
while (1): # TODO: exithook
cnn, addr = plainsocket.accept()
@@ -257,6 +271,8 @@ def _unixdomainhandler():
os.remove("/var/run/confluent/api.sock")
except OSError: # if file does not exist, no big deal
pass
if not os.path.isdir("/var/run/confluent"):
os.makedirs('/var/run/confluent', 0755)
unixsocket.bind("/var/run/confluent/api.sock")
os.chmod("/var/run/confluent/api.sock",
stat.S_IWOTH | stat.S_IROTH | stat.S_IWGRP |
@@ -289,14 +305,17 @@ def _unixdomainhandler():
class SockApi(object):
def __init__(self):
def __init__(self, bindhost=None, bindport=None):
self.tlsserver = None
self.unixdomainserver = None
self.bind_host = bindhost or '::'
self.bind_port = bindport or 13001
def start(self):
global auditlog
global tracelog
tracelog = log.Logger('trace')
auditlog = log.Logger('audit')
self.tlsserver = eventlet.spawn(_tlshandler)
self.tlsserver = eventlet.spawn(
_tlshandler, self.bind_host, self.bind_port)
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
+1 -1
View File
@@ -12,7 +12,7 @@ Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Requires: pyghmi, eventlet, greenlet, pycrypto >= 2.6.1, confluent_client, PyPAM, pyparsing
Requires: pyghmi, eventlet, greenlet, pycrypto >= 2.6.1, confluent_client, pyparsing
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
Url: http://xcat.sf.net/
+30
View File
@@ -0,0 +1,30 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['c:/Python27/Scripts/confluentsrv.py'],
pathex=[],
hiddenimports=['pyghmi.constants', 'pyghmi.exceptions', 'pyghmi.ipmi.console', 'pyghmi.ipmi.private.constants', 'pyghmi.ipmi.private', 'pyghmi.ipmi.private.session', 'pyghmi.ipmi.command', 'pyghmi.ipmi.events', 'pyghmi.ipmi.fru', 'pyghmi.ipmi.private.spd', 'pyghmi.ipmi.oem.lookup', 'pyghmi.ipmi.oem.generic', 'pyghmi.ipmi.oem.lenovo', 'pyghmi.ipmi.private.util', 'pyghmi.ipmi.sdr'],
hookspath=None,
runtime_hooks=None,
excludes=None,
cipher=block_cipher)
pyz = PYZ(a.pure,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='confluentsrv.exe',
debug=False,
strip=None,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
Tree('confluent/plugins', prefix='confluent/plugins'),
strip=None,
upx=True,
name='confluentsrv')
+6 -1
View File
@@ -1,3 +1,8 @@
cd `dirname $0`
VERSION=`cat VERSION`
VERSION=`git tag -l|tail -n 1`
NUMCOMMITS=`git rev-list $VERSION..HEAD|wc -l`
if [ "$NUMCOMMITS" -ne 0 ]; then
VERSION=$VERSION.dev$NUMCOMMITS.g`git rev-parse --short HEAD`
fi
echo $VERSION > VERSION
sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py
+1
View File
@@ -15,6 +15,7 @@ setup(
'pyghmi>=0.6.5'],
scripts=['bin/confluent'],
data_files=[('/etc/init.d', ['sysvinit/confluent']),
('/usr/lib/systemd/system', ['systemd/confluent.service']),
('/opt/confluent/lib/python/confluent/plugins/console/', [])],
)
@@ -0,0 +1,13 @@
# IBM(c) 2015 Apache 2.0
[Unit]
Description=Confluent hardware manager
[Service]
Type=forking
PIDFile=/var/run/confluent/pid
ExecStart=/opt/confluent/bin/confluent
ExecStop=/opt/confluent/bin/confetty shutdown /
[Install]
WantedBy=multi-user.target
+36 -15
View File
@@ -11,35 +11,56 @@
if [ -f /etc/init.d/functions ]; then
. /etc/init.d/functions
LOG_SUCCESS=success
LOG_FAILURE=failure
elif [ -f /lib/lsb/init-functions ]; then
. /lib/lsb/init-functions
LOG_SUCCESS=log_success_msg
LOG_FAILURE=log_failure_msg
else
echo "Unknown platform"
exit 1
fi
confluent=/opt/confluent/bin/confluent
confetty=/opt/confluent/bin/confetty
stop() {
echo -n 'Stopping Confluent: '
if [ -S /var/run/confluent/api.sock ]; then
$confetty shutdown /
fi
$LOG_SUCCESS
echo
return
}
start() {
echo -n 'Starting Confluent: '
$confluent
if [ $? -eq 0 ]; then
$LOG_SUCCESS
echo
return 0
else
$LOG_FAILURE
echo
return 1
fi
}
case $1 in
restart)
if [ -S /var/run/confluent/api.sock ]; then
echo -n 'Stopping Confluent '
/opt/confluent/bin/confetty shutdown /
fi
echo -n 'Starting Confluent '
/opt/confluent/bin/confluent
$LOG_SUCCESS
stop
start
;;
start)
echo -n 'Starting Confluent '
/opt/confluent/bin/confluent
$LOG_SUCCESS
start
;;
stop)
echo -n 'Stopping Confluent '
if [ -S /var/run/confluent/api.sock ]; then
/opt/confluent/bin/confetty shutdown /
fi
$LOG_SUCCESS
stop
;;
status)
status -p /var/run/confluent/pid $confluent
;;
esac