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

Compare commits

..

360 Commits

Author SHA1 Message Date
Jarrod Johnson bc5efa8a7e Try restarting confluent if running on update
This mitigates chance of confluent being stale.
2017-01-20 15:50:12 -05:00
Jarrod Johnson 486c322233 Have binding network sockets occur in a retry loop
There seems to be scenarios where a previously used socket won't open up immediately.  Retry when
this is detected.
2017-01-17 13:59:22 -05:00
Jarrod Johnson 548e4404ce Fix confluent startup error if dead
Clean up stale dbg.sock if present rather than fail to start.  Also, if the pid indicated in pid file doesn't exist, ignore
the pidfile contents.
2017-01-06 13:43:02 -05:00
Jarrod Johnson 99e97fe5c4 Add the noderun command to the manifest
The noderun command was not being packaged.  Correct the
oversight so that it will be in packages.
2017-01-06 13:29:25 -05:00
Jarrod Johnson 8c13e738c0 Make usage/help more consistent across the commands
Have every client command run argparse to get a chance at '-h'.  When
lacking arguments, always use print_help() to provide detail rather than usage.
2017-01-06 13:28:28 -05:00
Jarrod Johnson 5ffc2c298b Correct mixup of parentheses and brackets 2017-01-04 09:56:31 -05:00
Jarrod Johnson 7bf8242aba Assign self.ipmicmd in case we are reusing an existing object that is already logged. 2017-01-04 09:45:19 -05:00
Jarrod Johnson b3c28ad33e Defer assigning of self.ipmicmd until login
Wait until logged in to assign.  This way we have confirmation that ipmicmd is viable before trying to use it.
2017-01-04 09:17:07 -05:00
Jarrod Johnson e7bdb5ee7d Defer TLS key handler registration
Wait until the object is logged in before trying to set
the key handler.  It carries some prerequisite on talking
to the BMC, so it is better handled in the logged handler.
2017-01-03 16:00:27 -05:00
Jarrod Johnson 8eef064b9f Provide more useful error on socket error
If a socket error occurred that wasn't -2, the client would get an error
about KeyError, which wouldn't be helpful.  raise the unhandled exception
to provide more informative data in case of an issue.
2016-12-02 10:35:24 -05:00
Jarrod Johnson cb8cd29022 Add two more man page sources 2016-12-01 16:37:50 -05:00
Jarrod Johnson c016c55340 Add HTTP/1.0 cache suppression
There is not a single client that will ever talk
to this service that would not support HTTP/1.1.
However, do this to satisfy a scanner.
2016-11-30 11:42:50 -05:00
Jarrod Johnson d2156f3d67 Move the anti-referrer login to only logout
This has no functional difference, just a cosmetic
difference that does not give the erroneous impression
a logout actually occurred.  This does mean that if a browser
disables cookies and uses the api explorer, there would
be an opportunity for a CSRF.
2016-11-30 10:15:44 -05:00
Jarrod Johnson bf8dff90f3 Provide CSRF protection to logout resource
Though more annoying than harmful, guard against CSRF succeeding
against the logout resource.
2016-11-30 10:04:25 -05:00
Jarrod Johnson 004d40e7ca Implement more CSRF countermeasures
At some expense of convenience, make it difficult
for CSRF to succeed even against the API explorer
for most people.  User/Password will now only be
accepted on hitting enter in the address bar rather
than following any link.
2016-11-30 09:57:58 -05:00
Jarrod Johnson ac084b212e Merge branch 'nodeexec' 2016-11-29 10:50:32 -05:00
Jarrod Johnson 7557136d5d Add a number of security headers
There are a number of headers security scanners expect.
Explicitly declare how strict browser should be with
responses.
2016-11-29 10:49:02 -05:00
Jarrod Johnson 70762d3f6c Undo accidental change to nodesetboot 2016-11-08 14:51:57 -05:00
Jarrod Johnson eeb668bdfa Correct formatting page in nodesensors man page 2016-11-08 14:50:10 -05:00
Jarrod Johnson 61bd415ec4 Make confluent depend on python-dns
Fedora and family naming convention removes
'python' from the 'dnspython' name.  Work
within that standard to improve compatibility
with epel and such.
2016-11-07 14:07:12 -05:00
Jarrod Johnson 107337fdba Merge branch 'master' of github.com:jjohnson42/confluent 2016-11-07 10:19:41 -05:00
Jarrod Johnson eb02247a58 Fix consistency of sessionid
Regardless of whether the client uses it as a session id
or not, the fact remains a sessionid is assigned.
Pass the session id in the auth data even if the client
did not send it.
2016-11-07 10:18:32 -05:00
Jarrod Johnson 53904a2a5a Revert "Remove 'path' condition for session cookie"
This reverts commit 0ad4ae90c9.
2016-11-01 13:45:33 -04:00
Jarrod Johnson 5881ad8b68 Support sub-second interval
If a user requests an interval that is not a whole
number, begin honoring it and adjust the timestamp
precision to indicate milliseconds.  Do not bother
with milliseconds on whole number intervals.
2016-10-25 14:38:31 -04:00
Jarrod Johnson e0211fd8d8 Merge branch 'avoidrecursereplay' 2016-10-25 14:38:04 -04:00
Jarrod Johnson 0ad4ae90c9 Remove 'path' condition for session cookie
The cookie had an unnecessary limitation.  path need not be specified,
and it is possible for a client to request in a way that fails the criteria.
2016-10-21 09:56:14 -04:00
Jarrod Johnson 6f9bdf4a7c Create multiple files on roll conflict
Rather than removing the previous file, append a digit
to denote extra log files sharing the same roll date.
2016-10-18 11:28:47 -04:00
Jarrod Johnson 4eaab9db37 Avoid inifinite replay loop on bad previouslogfile
If circumstances result in a log file after rollover
pointing at itself, break out rather than looping trying
to get the same data from itself.
2016-10-18 11:18:57 -04:00
Jarrod Johnson 92fa2bf4d9 Add a number of security headers
There are a number of headers security scanners expect.
Explicitly declare how strict browser should be with
responses.
2016-10-13 11:08:05 -04:00
Jarrod Johnson 7f9394b33a Handle events without timestamps
If an event is missing a timestamp, omit rather than stack trace
2016-10-11 16:46:01 -04:00
Jarrod Johnson 05a6664165 Add a noderun command
This command enables running various commands against the nodes.
While I was at it, fix permissions on some files in git.
2016-10-06 16:30:29 -04:00
Jarrod Johnson 457f1fe30b Provide resource to allow clients to expand custom expressions
Clients may now format a string as if it were to be an expression for an attribute,
and have the server evaluate it using the same engine without passing through the
attribute engine.  This makes it easier, for example, to do nodeexec n1-n4 ipmitool -H {hardwaremanagement.manager}
2016-10-06 15:51:07 -04:00
Jarrod Johnson 2efadf21b5 Begin writing man pages
Provide a couple of man pages to get started on the effort
to provide man pages for the commands.
2016-10-04 15:10:53 -04:00
Jarrod Johnson bb38ff4588 Fix auto-version build
The build autoversion was not correct.  Use
git describe instead to be more complete.
2016-09-30 13:12:40 -04:00
Jarrod Johnson d4ffc41451 Provide means of asynchttp of passing exceptions
This allows client code to be actually same when
using synchronous versus asynchonous.
2016-09-28 15:00:49 -04:00
Jarrod Johnson fbd5059ade Unconditionally register key handler
For reasons not entirely understood, we can
have an ipmi command object without registering
a key handler.  Unconditionally register the key handler
on each use of the command object.  This still saves the
network load of logging in, so it's not a terrible workaround.
2016-09-28 14:59:43 -04:00
Jarrod Johnson baf8587759 Have the confluent message populate 'errorcode'
This allows web code framework to correctly handle responses
in noderange/async as if they were traditional straightforward rest calls.
2016-09-26 16:00:00 -04:00
Jarrod Johnson 7eb881d7e5 Add API code values to confluent messages
When a message is used that would raise an exception in a singleton
case, make available the appropriate apicode in contexts that would be
relevant (namely multi-node operations and all asynchttp).
2016-09-23 16:44:02 -04:00
Jarrod Johnson 9f0daf324e Refactor error code into exceptions
This makes the exceptions more self describing and simplifies httpapi and sockapi.
An important step to improve asynchttp conveyance of error data that would normally
be an http error in synchronous operation.
2016-09-23 16:32:53 -04:00
Jarrod Johnson ddbc155d6b Merge pull request #61 from aduffy19/fixIndexOutOfRangeError
Add exception check if passvalues is empty
2016-09-23 09:48:35 -04:00
Amanda Duffy 9d86ffee92 Add exception check if passvalues is empty 2016-09-22 16:58:00 -04:00
Jarrod Johnson 1286f8af3c Add facility for Anti-CSRF measures
Enable a WebUI to request an auth token.  This will allow it to indicate it is running in a browser and have the server implement protections such that
other software in the browser cannot send arbitrary requests into the server API.

This is implemented in a backward compatible fashion, allowing, for example, purely non-browser clients to ignore the CSRF protection as
it doesn't apply to that use case.
2016-09-22 11:09:05 -04:00
Jarrod Johnson d183ef768d Defer console startup until after API startup
Consoles starting up would potentially delay API availaility.  Change
by having the API having ample time to startup, then commence the
busy work of starting cnosole sessions.
2016-09-13 11:33:53 -04:00
Jarrod Johnson d19fdad0ba Avoid double-disconnect behavior
Do a better job of cleanly handling scenarios
where disconnect would come from a session currently
disconnected.  Inside the ipmi plugin, suppress a
disconnect event if one has been sent.  Inside
consoleserver, surpress logging a disconnect when
already disconnected.

Originally was going to skip the reconnect, but that would
mitigate recovery.  Hopefully supressing the duplicate
disconnect in ipmi plugin, and some fixes in pyghmi will
avoid a 'double connect' scenario.
2016-09-12 14:35:27 -04:00
Jarrod Johnson b503d9ca11 Fix nodeeventlog with 'None' entries
Some entries have 'None' fields.  Gracefully tolerate
this scenario.
2016-09-09 09:14:25 -04:00
Jarrod Johnson 9e4ee7bb31 Fix conflicts with system python modules
If python system module had a name that conflicted in some way
with a plugin, the plugin load would fail.  Fix this by prioritizing
the plugin path over system locations.  Also, to avoid the breakage
going the other way, remove the plugindir from the system path when
that particular directory is done.
2016-08-29 09:56:21 -04:00
Jarrod Johnson 4d04c1fb18 Add break and reopen to http consoles
HTTP console API did not have a means to send break
or request session reopen.  Rectify this discrepency
by adding an 'action' key to request certain console
specific actions.  In retrospect, closing the session
should have just been an 'action', but leaving things
as-is.
2016-08-23 14:04:20 -04:00
Jarrod Johnson 1085e342fd Make missing NTP server return 404
Before it was returning 500 because of index out of range
if a client was pulling an index unconditionally.
2016-08-08 09:11:59 -04:00
Jarrod Johnson 05e642ada5 Do not overwrite 'login' prompt in ssh plugin
ssh plugin was sending backspaces without bound, causing
deletion of the login prompt.
2016-08-04 16:44:18 -04:00
Jarrod Johnson 00da61b981 Enable backspace for ssh user/pass prompt
When prompting for username and password,
make backspace work fine.
2016-08-03 13:49:27 -04:00
Jarrod Johnson 786a1ec93e Fix a couple of formatting issues 2016-07-19 09:15:45 -04:00
Jarrod Johnson 27524ab3ce Merge pull request #60 from jjohnson42/switchsupport
Provide function to interrogate ethernet switches for location
2016-07-18 10:33:22 -04:00
Jarrod Johnson 7b160bd99c Fix namesmatch to actually return True
In the common case, we were falling through the bottom
without an explicit return.  Restructure things to both
explicitly return and look a bit more sane.
2016-07-15 16:47:42 -04:00
Jarrod Johnson 9516efd74a Merge branch 'master' into switchsupport 2016-07-14 11:01:07 -04:00
Jarrod Johnson 5410b394f2 Fix 'unset' on noderange
The Attributes management class was making shared shallow
copies.  This caused a problem when attributes class assumed
it could modify the result.  Correct by providing a deep copy
of that node's data when it is requested.
2016-07-14 09:56:40 -04:00
Jarrod Johnson 801a4c4b1e Merge branch 'switchsupport' of github.com:jjohnson42/confluent into switchsupport 2016-07-14 09:28:00 -04:00
Jarrod Johnson 29da853bcf Add mac map lookup against config to get node
This brings things right to the level of xCAT in
terms of underlying capability.  mac addresses have both
an all inclusive list of ports it is found on, and any nodes
that it matches.  It goes another step further by logging errors
when ambiguity is detected (either verbatim config conflict or
ambiguous result based on 'namesmatch' and the switch config).
2016-07-14 09:27:15 -04:00
Jarrod Johnson 7a72de6033 Improve behavior of mac map
One, include a number of 'fellow' mac addresses on the same port.
Another, allow a mac to appear on multiple ports and have that
reflected in the data structure.  Also capture errors to trace
log rather than hanging up on unexpected cases.
2016-07-14 09:27:15 -04:00
Jarrod Johnson b9733b3e0e Provide config enabled switch mapping
Wire up the singleton switch search function to a function that
extracts list of switches and relevant auth data from the config
engine.  Add attributes to allow indication by hardware management
port connection.  The OS nics will be added later for in-band discovery,
but that's of limited value until PXE support anyway.

This time, the update function is a generator that yields as a sign to caller
that the mac map has had at least a partial update to be considered.
2016-07-14 09:27:15 -04:00
Jarrod Johnson 4aeb7e1df5 Provide a simple global 'log' function
As we implement internal processes with automation,
provide a hook for code to convey information about
situations encountered during background activity.
Ultimately, it is intended to hook event forwarders
for things like syslog/email/etc
2016-07-14 09:27:15 -04:00
Jarrod Johnson 147b3952e0 Implement the next layer of switch discovery
Refactor the snmputil to be object oriented to simplify upstream code.  Implement
a method to generate a mac address to ifName/ifDescr for a given switch.
2016-07-14 09:27:15 -04:00
Jarrod Johnson 54e135f210 Add a util function for SNMP
On the path to instrumenting network switches, first
we'll add some framework for SNMP.  Given that we are
using eventlet and thus we need a patchable SNMP,
we employ PySNMP, despite it being a bit peculiar.
This commit tucks away the oddness and makes it
pretty easy to use for our purposes.
2016-07-14 09:27:15 -04:00
Jarrod Johnson 958be7d004 Fix 'cd' to /noderange/nr in confetty
The cd performance optimization caused a problem.  This
commit recognizes /noderange/ as special auto-vivifying
directory that must be 'gotten'.
2016-07-14 09:15:49 -04:00
Jarrod Johnson 7a4c9a1fc0 Add mac map lookup against config to get node
This brings things right to the level of xCAT in
terms of underlying capability.  mac addresses have both
an all inclusive list of ports it is found on, and any nodes
that it matches.  It goes another step further by logging errors
when ambiguity is detected (either verbatim config conflict or
ambiguous result based on 'namesmatch' and the switch config).
2016-07-14 08:55:50 -04:00
Jarrod Johnson 9764a02419 Improve behavior of mac map
One, include a number of 'fellow' mac addresses on the same port.
Another, allow a mac to appear on multiple ports and have that
reflected in the data structure.  Also capture errors to trace
log rather than hanging up on unexpected cases.
2016-06-30 15:54:18 -04:00
Jarrod Johnson f539a4e4b6 Provide config enabled switch mapping
Wire up the singleton switch search function to a function that
extracts list of switches and relevant auth data from the config
engine.  Add attributes to allow indication by hardware management
port connection.  The OS nics will be added later for in-band discovery,
but that's of limited value until PXE support anyway.

This time, the update function is a generator that yields as a sign to caller
that the mac map has had at least a partial update to be considered.
2016-06-29 16:32:46 -04:00
Jarrod Johnson 6b5f437a1c Provide a simple global 'log' function
As we implement internal processes with automation,
provide a hook for code to convey information about
situations encountered during background activity.
Ultimately, it is intended to hook event forwarders
for things like syslog/email/etc
2016-06-29 11:29:05 -04:00
Jarrod Johnson 8387f0e13e Implement the next layer of switch discovery
Refactor the snmputil to be object oriented to simplify upstream code.  Implement
a method to generate a mac address to ifName/ifDescr for a given switch.
2016-06-29 11:26:46 -04:00
Jarrod Johnson ee679b745e Add a util function for SNMP
On the path to instrumenting network switches, first
we'll add some framework for SNMP.  Given that we are
using eventlet and thus we need a patchable SNMP,
we employ PySNMP, despite it being a bit peculiar.
This commit tucks away the oddness and makes it
pretty easy to use for our purposes.
2016-06-28 14:21:21 -04:00
Jarrod Johnson 3c876566a6 Switch to green DNS host resolution
The stock getaddrinfo can hang up all of confluent if misbehaving.
Patch pyghmi and switch to using greendns in confluent internal lookups.
2016-06-01 09:15:16 -04:00
Jarrod Johnson f85ee82df3 Revert last portion of attempt at console auto-health
For now, it's causing more problems than it solved.  Back out until
a more appropriate time to investigate.
2016-05-25 13:09:10 -04:00
Jarrod Johnson 20abffdbee Revert "After 60 seconds of 'connect' limbo, kick a connection attempt"
This reverts commit e4aa873141.
There may be some memory consumption issues with this feature.
2016-05-25 10:59:22 -04:00
Jarrod Johnson 2dd44b1725 Correct typo 2016-05-24 14:38:44 -04:00
Jarrod Johnson f4e8dd497f Add missing utility commands to manifest 2016-05-24 14:36:36 -04:00
Jarrod Johnson 9a93baed0e Fix handling of unicode data in inventory
It is possible for unicode data to appear in some data values.  Use a unicode
string to hold the value, in case of unicode data coming from server.
2016-05-23 15:36:30 -04:00
Jarrod Johnson 41e84c7c47 Remove explict console health check
This is a resource consumption problem.  Defer such measures until later.
Investigation uncovered that there may have been another culprit anyway,
will see if only the other change (to kick a zoned out connection attempt)
suffices.
2016-05-23 13:49:46 -04:00
Jarrod Johnson a046e4939f Fix ping before connection error
If ping() was called before connect, an exception was raised.  Fix this by
returning false in such an event.
2016-05-19 16:32:56 -04:00
Jarrod Johnson e4aa873141 After 60 seconds of 'connect' limbo, kick a connection attempt
Occasionally it was observed that systems would be just stuck in 'connect',
provide a backup system to detect and forcibly kick the console in such a case.
2016-05-19 15:39:04 -04:00
Jarrod Johnson ec02097b52 Explicitly check IPMI console health
In theory, pyghmi should be doing a self-health check.  It has been discovered at scale that
this self-health check may encounter issues.  For now, try to workaround by having another
health check at the confluent level, deferred by console activity.  It's also spaced far apart
so it should not significantly add to idle load (one check every ~5 minutes, spread out).
2016-05-19 14:44:28 -04:00
Jarrod Johnson 5d105c43e5 Add option to skip numberless
Many sensors in nodesensors are not useful except when
evaluated as part of nodehealth.  Provide an option to allow people
to skip such sensors.  Particularly useful in generating time series CSV
data.
2016-05-12 15:53:55 -04:00
Jarrod Johnson ca91cfb220 Add nodefirmware command
This command currently enumerates current firmware on the target.  In the future it may be extended to update.
2016-05-12 11:04:26 -04:00
Jarrod Johnson b328c53d91 Fix error handling for nodeinventory command
Cleanly handle error messages from server
2016-05-12 10:25:35 -04:00
Jarrod Johnson 129f034c07 Provide some more friendly string values
Some keys from the API are a little weird, provide a mapping for them.
2016-05-12 09:19:30 -04:00
Jarrod Johnson b5fbfe730d Add nodeinventory command
Provide a native confluent client alternative to 'rinv'.
Also add missing flags to nodesetboot.
2016-05-11 17:03:05 -04:00
Jarrod Johnson d9e47824a4 Backoff automatic reconnect interval
Previously, offline nodes would be rechecked automatically on average every 45 seconds.  Extend this
to on average 180 seconds, to reduce ARP traffic significantly when there are a large volume of
undefined nodes.  The 'try to connect on open' behavior is retained, so this would mean a longer loss
of connectivity only in a background monitored session.
2016-05-11 13:33:36 -04:00
Jarrod Johnson 96670784f9 Automatically increase limits
Knowing ahead of time that confluent is the sort of app that, despite
best efforts, is filehandle heavy, auto-attempt to raise soft to
be equal to hard limit.  A sufficiently large cluster (i.e. more than 2000
nodes) would still need to have limit adjusted at system level for now.
2016-05-10 14:44:52 -04:00
Jarrod Johnson 16c7429900 Improve interactive performance of 'cd' to slow collections
Sometimes a collection will be slow.  Don't inflict the 'cd' with the slowness, defer until actually
asked to do something that would enumerate said collection.  Accomplish this by checking for
the 'cd' target in it's parent collection, rather than asking to list its contents.
2016-05-09 15:39:05 -04:00
Jarrod Johnson 14f6fabe0a Do not trigger AttributeError on Null event
In the scenario where event is present but 'None', handle the situation more gracefully, by ignoring it's existance.
2016-05-09 13:59:50 -04:00
Jarrod Johnson d5e833480e Tolerate gdbm
gdbm backend does not support the 'iterkeys' interface directly,
requiring instead to manually traverse.  Unfortunately, dbhash
does not implement the gdbm interface for this, so we have
to have two codepaths.
2016-05-02 10:44:12 -04:00
Jarrod Johnson e949ee932a Implement 'persistent' option for nextdevice
Some systems provide the functionality, provide the message support
to do that.
2016-04-28 13:11:25 -04:00
Jarrod Johnson b524af08b3 Add back explicit patching of portions of pyghmi
The previous commit produced significant problems.  pyghmi
late binds those values, so they must be explicitly patched.
2016-04-22 17:15:17 -04:00
Jarrod Johnson df74753908 Patch import of pyghmi
Now that the problematic use of an os pipe is no more,
go ahead and patch pyghmi in a straightforward way.  This
was needed for the sake of pyghmi plugins that use a webclient.
2016-04-22 17:01:56 -04:00
Jarrod Johnson bb0e256a98 Convert datetime objects to ISO8601 on the way out
If a plugin iterates a datetime object, decode to ISO-8601 string
on the way out.  This allows plugins to work directly with datetime
objects and allow the messaging layer to normalize it to ISO-8601
2016-04-20 16:51:01 -04:00
Jarrod Johnson 26da687dc3 Do not organize 'databynode' when not node
Messages that were not a node (e.g. confluent users) erroneously
had data put into 'databynode'.  Correct the mistake by omitting
the insertion of databynode when the message is clearly not a node
related thing.
2016-04-14 13:31:54 -04:00
Jarrod Johnson fa3a402708 Provide some shortcuts for nodelist
Allow nodelist to request view of a category at a time.
Also recognize 'hm' as shorthand for 'hardwaremanagement'.
2016-04-12 15:18:31 -04:00
Jarrod Johnson 0672666e42 Assure that get_health always updates inhealth
If an unforseen circumstance occurs while trying to get health,
make sure we recognize that scenario.
2016-04-12 14:16:15 -04:00
Jarrod Johnson d4357c6984 Avoid double-removal of attrib watcher in ipmi
IPMI plugin was issuing redundant calls to remove the same
watcher.  Track that a session has already unhooked to
avoid double unhook (which runs at least a slight risk
of unhooking the wrong handler (*if* it were allowed).
2016-04-12 13:04:02 -04:00
Jarrod Johnson 4ba8a7a997 Dedupe concurrent ipmi health requests
IPMI health requests are relatively expensive.  It's
also pretty popular and therefore prone to be the target of
inadvertantly aggressive concurrent requests.  Mitigate the harm
by detecting concurrent usage and having callers share an answer.
2016-04-12 10:28:01 -04:00
Jarrod Johnson fa0c0ce81a Add paramiko and update package names in server 2016-04-11 13:06:05 -04:00
Jarrod Johnson d3bda4217c Add paramiko to the requirements 2016-04-11 11:51:11 -04:00
Jarrod Johnson 22509946c0 Reduce verbosity of audit log
There are a number of pretty innocuous requests that
need not be individually tracked.  For such requests,
we'll abstain from putting it into the log.
2016-04-08 16:51:32 -04:00
Jarrod Johnson f8b878b5f4 Unhook attribute watch on dead sessions
When a session is dead, it need not be told about
changes to config.  Save time and sanity by reaping
when discarding a dead session.
2016-04-05 13:57:47 -04:00
Jarrod Johnson 91a1c0ef7d Fix key registration to happen on success
Key registration was attempted either way, causing bad targets
to fail to return timely error data.
2016-04-05 11:34:23 -04:00
Jarrod Johnson 419fcf1577 Defer key registration until login
Part of key registration is giving the OEM handler
a crack at it.  For that reason, defer the registration
until after login process has occurred.
2016-04-05 10:59:20 -04:00
Jarrod Johnson 06e767e70e Fix handling of error messages in async
ConfluentNodeError branch of messages were not recognized.  Correct the oversight.
2016-03-28 08:54:33 -04:00
Jarrod Johnson 94d2be4a87 Merge pull request #59 from jjohnson42/multiplex
Background HTTP Function
2016-03-26 13:43:10 -04:00
Jarrod Johnson 2ea7ee0dcb Add thread traces to USR1 handler
When receiving a USR1 signal, it did usefully provide
'the' current stack, useful for diagnosing really hard
hangs.  However, it's frequently informative to see all
the thread stack traces, so add that data to the diagnostic
feature.
2016-03-26 13:34:21 -04:00
Jarrod Johnson 417e70e5c1 Tolerate terminal closure
When a terminal closes and notifies server, it was
pulling the rug out from asyncsession consoles.
Make asyncsession aware that the console may be gone
and discard tracking it rather than give a 500.
2016-03-26 10:45:47 -04:00
Jarrod Johnson 03b2cdab5a Assure console sessions get reaped
When an error (to be fixed) happened while updating expiry,
an asyncsession failed to have a reaper scheduled for cleanup.
Correct this by putting the reaper schedule right after the
cancellation.

Further, an async being destroyed did not reap related console
sessions.  Add code to reap related console sessions when
the async session gets destroyed.
2016-03-26 10:26:17 -04:00
Jarrod Johnson 79b1268a75 Tolerate cp437 format text
UEFI output may still be cp437.  Tolerate through
attempting to use it.  UTF-8 continues to be preferred.
2016-03-26 10:02:16 -04:00
Jarrod Johnson 50aefee728 Correct a number of issues
There were a number of careless mistakes in the feature, correct
the bad usage and typos.
2016-03-26 09:34:46 -04:00
Jarrod Johnson 44a5c2b464 Merge branch 'master' into multiplex 2016-03-25 16:47:23 -04:00
Jarrod Johnson 2dd6c31513 Fix deleted logs breaking partial buffer rebuild
When the read_recent_text ran off a cliff looking for buffer data,
it left the current textfile handle in a bad state.  This caused
the buffer rebuild to fail completely in a scenario where all the
current logs put together don't have enough data to satisfy the
buffer.  Fix this by making the handle more obviously broken, and
repairing while seeking out data.
2016-03-25 16:44:28 -04:00
Jarrod Johnson d753ac2833 Add terminal sessions to async http
This functionality enables a browser to hold more terminals open
than their max connection rating would normally allow.
2016-03-25 14:50:47 -04:00
Jarrod Johnson 3cd96a4f59 Force asyncresponse http to be JSON array
Rather than let it be ambiguous, force it to provide a JSON array.
2016-03-21 10:22:41 -04:00
Jarrod Johnson 2b3d5f7b62 Have async sessions detect logout 2016-03-21 10:22:41 -04:00
Jarrod Johnson 75a747a6a2 Amend structure of AsyncMessage
This is an easier structure to traverse for a client.
2016-03-21 10:22:41 -04:00
Jarrod Johnson 8fac1ce5da Fix up the async http to actually function
Still need to review the return data to determine best format
2016-03-21 10:22:41 -04:00
Jarrod Johnson 7d67ea0685 Refine asyncsupport
Asyncsupport progress continues.  Renaming from 'multiplex'
as 'async' seems to describe the pattern better.
2016-03-21 10:22:41 -04:00
Jarrod Johnson bcb9c2660f Implement a multiplex facility (WIP)
Allow an arbitrary number of HTTP requests using a
small pool of connections, as is likely in a
common web browser.
2016-03-21 10:22:41 -04:00
Jarrod Johnson 6504acecad Change default log retention to be indefinite
Users have noted and complained that log data was lost, and didn't have old data.  This changes
the default behavior to be indefinite retention.  Users noting a lot of logs using space have a nice
intuitive indication of old files to delete, and the option remains for those to request a log expiration.
2016-03-21 09:57:23 -04:00
Jarrod Johnson d1247cfb37 Restore disconnect notification to ssh plugin
The disconnect notification was erroneously removed in
the previous checkin, this restores it.
2016-03-16 11:20:14 -04:00
Jarrod Johnson c5e19fe474 Have ssh plugin report on connection error
Before the connection would fail and log to trace without anything
particularly informative for the client (they just saw 'unexpected error'.
Provide a more informative behavior for the client.
2016-03-16 09:50:46 -04:00
Jarrod Johnson 58bf72d5aa Do not remove databuffer on close
If exiting from a shell session, the databuffer will contain needed info for the client
to work properly.  Preserve databuffer existence.  Responsibility for deleting the
object should be in the hands of the caller.
2016-03-16 09:09:24 -04:00
Jarrod Johnson f15cf014e9 Avoid changing hash size during loop
Coerce iterator into a list so that for loop does not
raise an exception.
2016-03-16 08:40:39 -04:00
Jarrod Johnson fb1e20906e Do not worry over non-existant debug socket
If the socket was not created, do not error on exit because it isn't there to be cleaned up.
2016-03-15 11:15:15 -04:00
Jarrod Johnson 1bf124494e Add location attributes
Provide data that may be used to track system
locations.
2016-03-14 09:16:46 -04:00
Jarrod Johnson 9d40c67974 Support walking back through multiple logs
The rollback support and replaydid not follow more than one log back.  Do the work to recurse
into older and older files, until big enough buffer or run out of files.
2016-03-13 19:50:02 -04:00
Jarrod Johnson f75f2cae51 Correct sockapi behavior when user authorize returns None
If a user can connect, but gets removed mid session, traces were
being generated.  Correct by recognizing the circumstance and returning
the appropriate error to the client.
2016-03-13 18:57:27 -04:00
Jarrod Johnson 5ae0f37f97 Do not generate trace on request to delete non-existant session 2016-03-13 18:51:18 -04:00
Jarrod Johnson 7ff20e3e39 Close shell interface on remote disconnect 2016-03-13 18:48:58 -04:00
Jarrod Johnson 0e42e83c50 Restore intended per-user ssh sessions
Each user should have their own ssh sessions, as originally
intended.
2016-03-13 18:43:57 -04:00
Jarrod Johnson 378df2966f Clean up the debug socket
Also limit permissions of the socket.
2016-03-13 17:29:10 -04:00
Jarrod Johnson b6546f923b Fix security key initialization race condition
When initializing security key, a background thread may occur.  Sometimes,
the system would go to daemonize while that thread was still running, and
the whole system could exit.  Leading to incomplete write to globals as well
as leaving the daemon looking at the data copied over from pre-fork and
seeing the last state of that thread forever frozen.  Make sure the background
threads are fully done prior to exiting.
2016-03-08 11:34:25 -05:00
Jarrod Johnson 40007a6a07 Enable debug socket for sufficiently new eventlet
Newer versions of eventlet address the bug in backdoor when used with
unix domain sockets.  Detect and take advantage of that circumstance.
2016-03-07 16:44:11 -05:00
Jarrod Johnson b98889b54a Ensure correct inflight thread id is discarded
It seems it is possible in some circumstance for the thread id to become stale,
perhaps due to a different threadid executing the code for some reason.
Just in case, ensure the same exact value that was added is later discarded.
2016-03-05 15:47:49 -05:00
Jarrod Johnson 8bf7a55b68 Prevent double firing of event in consolesession
There is a timing scenario where an event could be double fired.
Prevent by clearing the event when sent.
2016-03-05 15:42:17 -05:00
Jarrod Johnson e9f2d7eb63 Improve logout return to browsers
A browser may choke on non-JSON if promised JSON.
Fix this by passing down JSON for now (API explorer
users can deal with seeing JSON for error data)
2016-02-29 09:15:21 -05:00
Jarrod Johnson 5ab6a9e7b7 Provide client hook to get session info
Web client may come in without knowing the session info.
Provide it additional data to populate UI elements.
2016-02-28 18:48:18 -05:00
Jarrod Johnson 64751bccee Add closesession request to term sessions
This provides a method for client to request session be closed down.  This provides more
immediate responsiveness to the client count when closing such a terminal.  With this
both closing a single window and doing a 'logout' immediately impacts clientcount.
2016-02-28 14:15:08 -05:00
Jarrod Johnson 244f655055 Suppress browser cache for API requests
Browser caching can interfere with our goal of delivering fresh data.
Suppress the cache behavior for our API.
2016-02-27 18:40:05 -05:00
Jarrod Johnson 1b26b2cf3d Reap session immediately on logout
When a logout happens in httpapi, immediately reap related console sessions that are in flight.
2016-02-27 14:04:19 -05:00
Jarrod Johnson 029c06cc66 Have polling consoleserver sessions more robustly clean up
If anything goes wrong or a session was exited, no destruction of the
session would be scheduled.  Always have a reaper scheduled for that.
2016-02-27 13:37:10 -05:00
Jarrod Johnson 1df60ceb73 Rename '/session/logout' to '/sessions/current/logout'
Have room for a future where a user may list and disconnect
other sessions.
2016-02-27 13:23:02 -05:00
Jarrod Johnson 875cda00ff Implement immediate logout
If something triggers a logout of session, immediately cut into long polling
console sessions that are relevant.  This results in web client being able to
immediately detect a logout externally originated.
2016-02-27 13:20:08 -05:00
Jarrod Johnson f20cdfe49a Add '/session/logout' to http api
Provide a means for an http request to erase
it's own session's validity.  Always return 200
to allow a client to send bogus credentials and
think they got success to forget the auth data in
the browser.
2016-02-27 11:40:26 -05:00
Jarrod Johnson ba6b7cf517 Give client method to suppress auth header
A javascript client running in browser may want
the standard authorization header suppressed.
This allows a client to block the default browser
authentication prompt.
2016-02-26 08:35:51 -05:00
Jarrod Johnson 76ff9fd759 Reduce long poll console to 25 seconds
A common proxy timeout is 30 seconds.  Adjust the
polling length to accomodate such a limitation.
2016-02-23 10:35:58 -05:00
Jarrod Johnson 18280ccd8a Fix bad location of auth check
Auth check was erroneously in a place without
ability to check the authenticated state.  Move
the check up the stack.
2016-02-21 15:05:11 -05:00
Jarrod Johnson 37f1acae1d Merge pull request #58 from jjohnson42/sshkey
SSH known hosts handling
2016-02-21 15:00:19 -05:00
Jarrod Johnson 44103b31f8 Extend key error data
Clients can now more consistently tell the difference between
a new key and a mismatch.
2016-02-21 14:44:31 -05:00
Jarrod Johnson 774d592eb4 Fix more usage mistakes 2016-02-11 12:08:18 -05:00
Jarrod Johnson 824253ae8c Hook the custom keyhandler policy
This actually uses the previously commited class, with one fix for
the structure of the key as passed into the callback.
2016-02-11 11:35:20 -05:00
Jarrod Johnson a574c69535 Implement SSH host key management
Like self-signed TLS certificates, SSH host keys
warrant a similar security policy.  This implementations
follows the lead of the TLS management and uses the same
policy name and interpretation, just storing the value
in 'pubkeys.ssh' for the node rather than an extensible
set of entry points (for now).
2016-02-11 09:13:21 -05:00
Jarrod Johnson a2445e7f65 Have client exit on auth issue
Without this, an auth error will leave client just
hanging without any feedback.  Correct this with
raising an exception for now.
2016-01-28 10:26:28 -05:00
Jarrod Johnson 0b51edde97 Merge branch 'jjohnson42-fixwget' 2016-01-28 10:19:17 -05:00
Jarrod Johnson 6b014deb04 Fix response to wget and similar
If the client comes in without an HTTP_ACCEPT at all,
do not trigger a trace.
2016-01-28 10:17:06 -05:00
Jarrod Johnson 366de1235c Merge pull request #55 from jjohnson42/sshconsole
Implement shell infrastructure with ssh support
2016-01-08 15:06:26 -05:00
Jarrod Johnson 1d67f10432 Merge branch 'sshconsole' of github.com:jjohnson42/confluent into sshconsole 2016-01-08 14:55:17 -05:00
Jarrod Johnson 6e1adc88dd Have sessions deleted on disconnect
When the shell session disconnects, destroy all dependent
sessions.
2016-01-08 14:44:52 -05:00
Jarrod Johnson 2419d95b74 Rework session to console relationship
Previously, was using counters to track the relation, also had distinct tracking of users versus
callbacks.  Unify the callback and user into a single 'session' attach and then use the size
of the set of sessions and their declared users rather than trying to maintain a counter on the side.

This change simplifies the relationship, changes away the logging and clientcount counter for
a more robust strategy, and paves the way for the dependent ShellHandler to terminate connected
sessions when the shell session dies.
2016-01-08 14:44:49 -05:00
Jarrod Johnson 62801734ab Have shell sessions not autoreconnect
Unlike consoles, where the underlying concept is a real
persistent thing that needs some care to reattach to watch,
a shell session should die when it is lost, as a new one would
have to be created anyway.  Modify the disconnect behavior
for a shell session to set closed and notify the receivers.
It should also reap dependent watching objects in a future
change.
2016-01-08 14:44:47 -05:00
Jarrod Johnson cf16bfdd95 Have builds include shell plugins
The builds were excluding the ssh plugin, correct the oversight.
2016-01-08 14:44:45 -05:00
Jarrod Johnson d0bd275cb3 Instrument sessions/ collection
This causes some additional features into core.  Namely
the ability to use a fixed module rather than a string
defined plugin.  This allows shellserver to implement the
'plugin' interface without living in 'plugins'.  'plugins'
implies modularity and potential eventual choice, but
this functionality is core.  It would make sense for the
'attributes' plugin to be changed to match this strategy.
2016-01-08 14:44:42 -05:00
Jarrod Johnson a332678312 Avoid double connect on shell start
When a shell session is initiated, it registers
a recipient at the same time it would be trying
to establish session for not being a 'wait for
recipient'.  Aggressively mark the state as connecting
to avoid the recipient erroneously thinking things have
not be set into motion yet.  Additionally, have the ssh
plugin avoid a traceback when disconnecting before completing
connection.
2016-01-08 14:44:39 -05:00
Jarrod Johnson cfafa5a5bc Connect shellserver to httpapi
Have httpapi recognize the difference and start a shellserver
session when appropriate.  Next step will be to wire up enumeration of
current shellserver sessions, debug ssh.py traceback, delete on remote
close, and auto-delete when no client connected after some interval (e.g.
30 minutes).
2016-01-08 14:44:37 -05:00
Jarrod Johnson 23f025eb71 Use a plain incrementing session id
UUIDs when a simple number will do are harder to use.
Change to a simple increment id.  This could cause an issue
with multiple management nodes, but I think the sessions
should be contained to the instance used.
2016-01-08 14:44:34 -05:00
Jarrod Johnson 5695bf5288 Fix misuse of console log for shell sessions
console logging assumptions are not valid for shell sessions.
Correct by modifying the buffer init code to be conditional
and adding a stub 'log' to the ShellHandler class.
2016-01-08 14:44:32 -05:00
Jarrod Johnson 3d926bb264 Amend core to pass shell objects through
For 'handler' plugin definitions, also recognize
the 'console' exception to allow the plugin to pass
through to the shellserver infrastructure.
2016-01-08 14:44:29 -05:00
Jarrod Johnson c8e5644061 Refactor socket api terminal handler
The 'terminal' handler code is broken out to get ready
for using 'shell' as well as 'console'
2016-01-08 14:44:27 -05:00
Jarrod Johnson ff857bce14 Retrofit consoleserver and make shellserver
Provide a common 'shellserver' capability cloned off of 'consoleserver'.
This will enable the concept of per-user shells with option for multiple
shells per.  Each user will have their own set of shell sessions rather
than shared across users.  Can revisit in future if sharing between
users is desired.
2016-01-08 14:44:25 -05:00
Jarrod Johnson 9146fce016 Add internal resource for 'shell' 2016-01-08 14:44:22 -05:00
Jarrod Johnson afa67f9bf7 Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2016-01-08 14:44:13 -05:00
Jarrod Johnson d54e31a1c5 Merge pull request #54 from jjohnson42/tlspolicies
Implement strategy for dealing with TLS targets
2016-01-08 14:40:15 -05:00
Jarrod Johnson 15ddb554f9 Have sessions deleted on disconnect
When the shell session disconnects, destroy all dependent
sessions.
2016-01-08 14:39:01 -05:00
Jarrod Johnson 4a660d2fb1 Rework session to console relationship
Previously, was using counters to track the relation, also had distinct tracking of users versus
callbacks.  Unify the callback and user into a single 'session' attach and then use the size
of the set of sessions and their declared users rather than trying to maintain a counter on the side.

This change simplifies the relationship, changes away the logging and clientcount counter for
a more robust strategy, and paves the way for the dependent ShellHandler to terminate connected
sessions when the shell session dies.
2016-01-08 11:23:55 -05:00
Jarrod Johnson 849193cf98 Have shell sessions not autoreconnect
Unlike consoles, where the underlying concept is a real
persistent thing that needs some care to reattach to watch,
a shell session should die when it is lost, as a new one would
have to be created anyway.  Modify the disconnect behavior
for a shell session to set closed and notify the receivers.
It should also reap dependent watching objects in a future
change.
2016-01-07 16:51:22 -05:00
Jarrod Johnson 00feca7e5b Have builds include shell plugins
The builds were excluding the ssh plugin, correct the oversight.
2016-01-07 16:15:59 -05:00
Jarrod Johnson be75018609 Instrument sessions/ collection
This causes some additional features into core.  Namely
the ability to use a fixed module rather than a string
defined plugin.  This allows shellserver to implement the
'plugin' interface without living in 'plugins'.  'plugins'
implies modularity and potential eventual choice, but
this functionality is core.  It would make sense for the
'attributes' plugin to be changed to match this strategy.
2016-01-07 15:36:44 -05:00
Jarrod Johnson a891745386 Avoid double connect on shell start
When a shell session is initiated, it registers
a recipient at the same time it would be trying
to establish session for not being a 'wait for
recipient'.  Aggressively mark the state as connecting
to avoid the recipient erroneously thinking things have
not be set into motion yet.  Additionally, have the ssh
plugin avoid a traceback when disconnecting before completing
connection.
2016-01-07 10:30:30 -05:00
Jarrod Johnson 46d3779774 Connect shellserver to httpapi
Have httpapi recognize the difference and start a shellserver
session when appropriate.  Next step will be to wire up enumeration of
current shellserver sessions, debug ssh.py traceback, delete on remote
close, and auto-delete when no client connected after some interval (e.g.
30 minutes).
2016-01-06 17:03:10 -05:00
Jarrod Johnson 5d73548583 Use a plain incrementing session id
UUIDs when a simple number will do are harder to use.
Change to a simple increment id.  This could cause an issue
with multiple management nodes, but I think the sessions
should be contained to the instance used.
2016-01-06 11:40:31 -05:00
Jarrod Johnson 8ae8b79837 Fix misuse of console log for shell sessions
console logging assumptions are not valid for shell sessions.
Correct by modifying the buffer init code to be conditional
and adding a stub 'log' to the ShellHandler class.
2016-01-06 11:27:11 -05:00
Jarrod Johnson 0df21ddeb0 Amend core to pass shell objects through
For 'handler' plugin definitions, also recognize
the 'console' exception to allow the plugin to pass
through to the shellserver infrastructure.
2016-01-06 10:06:09 -05:00
Jarrod Johnson 0e821a7bfe Refactor socket api terminal handler
The 'terminal' handler code is broken out to get ready
for using 'shell' as well as 'console'
2016-01-05 16:29:21 -05:00
Jarrod Johnson 30ed563810 Retrofit consoleserver and make shellserver
Provide a common 'shellserver' capability cloned off of 'consoleserver'.
This will enable the concept of per-user shells with option for multiple
shells per.  Each user will have their own set of shell sessions rather
than shared across users.  Can revisit in future if sharing between
users is desired.
2016-01-05 15:34:52 -05:00
Jarrod Johnson 89edc020d5 Use correct exception params on manual policy
In the rework of the exception to include full certificate
in body, a call was missed in the verify_cert() routine.
2015-12-01 09:11:08 -05:00
Jarrod Johnson e2b79a063d Provide full certificate to client on error
When a certificate error happens, provide full certificate data to the
caller so that they may implement whatever strategy they feel appropriate.
2015-11-30 12:32:14 -05:00
Jarrod Johnson f6ce9f2c1e Add infrastructure for TLS certificate handling
When connecting to peer devices that use TLS, provide
a mechanism of tracking peer fingerprint and handling
missing or mismatch of fingerprint.
2015-11-25 11:39:48 -05:00
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 dfc1e32546 Merge branch 'sshconsole' of github.com:jjohnson42/confluent into sshconsole 2015-11-03 09:06:59 -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 d03ca6eafe Add internal resource for 'shell' 2015-10-28 10:50:27 -04:00
Jarrod Johnson 4d148751e5 Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-28 10:50:22 -04: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 36dd493021 Merge branch 'sshconsole' of github.com:jjohnson42/confluent into sshconsole 2015-10-21 16:48:54 -04:00
Jarrod Johnson a3e5388303 Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-21 16:48:41 -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 2ff97b93ef Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-21 13:42:31 -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
57 changed files with 4964 additions and 723 deletions
+1
View File
@@ -0,0 +1 @@
include confluent_env.sh
-1
View File
@@ -1 +0,0 @@
1.1
+79 -22
View File
@@ -2,7 +2,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -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 = {}
@@ -82,11 +87,13 @@ laststate = {}
def updatestatus(stateinfo={}):
status = consolename
info = []
for statekey in stateinfo.iterkeys():
for statekey in stateinfo:
laststate[statekey] = stateinfo[statekey]
if ('connectstate' in laststate and
laststate['connectstate'] != 'connected'):
info.append(laststate['connectstate'])
if laststate['connectstate'] == 'closed':
quitconfetty(fullexit=consoleonly)
if 'error' in laststate:
info.append(laststate['error'])
# error will be repeated if relevant
@@ -248,6 +255,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 +268,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(
@@ -296,12 +309,38 @@ def do_command(command, server):
target = fullpath_target(argv[1], forcepath=True)
else: # cd by itself, go 'home'
target = '/'
for res in session.read(target, server):
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
if target[-1] == '/':
parentpath = target[:-1]
else:
parentpath = target
if parentpath:
childname = '{0}/'.format(parentpath[parentpath.rindex('/') + 1:])
parentpath = parentpath[:parentpath.rindex('/') + 1]
if parentpath == '/noderange/':
for res in session.read(target, server):
if 'errorcode' in res:
exitcode = res['errorcode']
target = otarget
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
else:
foundchild = False
for res in session.read(parentpath, server):
try:
if res['item']['href'] == childname:
foundchild = True
except KeyError:
pass
if 'errorcode' in res:
exitcode = res['errorcode']
target = otarget
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
if not foundchild:
sys.stderr.write(target + ': Target not found - \n')
target = otarget
elif argv[0] in ('cat', 'show', 'ls', 'dir'):
if len(argv) > 1:
targpath = fullpath_target(argv[1])
@@ -355,9 +394,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 +530,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 +629,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 +645,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 +699,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]))
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime as dt
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 (clear)")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
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 and evt['timestamp'] is not None:
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 and evt['component_type'] is not None:
dscparts.append(evt['component_type'])
if 'component' in evt and evt['component'] is not None:
dscparts.append(evt['component'])
if 'event' in evt and evt['event'] and evt['event'] is not None:
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))
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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
exitcode = 0
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
def printfirm(node, prefix, data):
if 'model' in data:
prefix += ' ' + data['model']
builddesc = []
if 'build' in data:
builddesc.append(data['build'])
if 'date' in data:
builddesc.append(data['date'])
if 'version' in data:
version = data['version']
if builddesc:
version += ' ({0})'.format(' '.join(builddesc))
else:
version = ' '.join(builddesc)
print('{0}: {1}: {2}'.format(node, prefix, version))
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
try:
session = client.Command()
for res in session.read('/noderange/{0}/inventory/firmware/all/all'.format(
noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
for prefix in inv:
printfirm(node, prefix, inv[prefix])
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+60 -37
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
# limitations under the License.
import codecs
import optparse
import os
import sys
@@ -28,42 +29,64 @@ import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
noderange = sys.argv[1]
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
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)
+9 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
@@ -25,8 +26,14 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
noderange = sys.argv[1]
identifystate = None
if len(sys.argv) > 2:
identifystate = sys.argv[2]
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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
def pretty(text):
if text == 'pcislot':
return 'PCI slot'
if text == 'partnumber':
return 'part number'
return text
def print_mem_info(node, prefix, meminfo):
capacity = meminfo['capacity_mb'] / 1024
memdescfmt = '{0}GB PC'
if meminfo['memory_type'] == 'DDR3 SDRAM':
memdescfmt += '3-{1} '
elif meminfo['memory_type'] == 'DDR4 SDRAM':
memdescfmt += '4-{1} '
if meminfo['ecc']:
memdescfmt += 'ECC '
memdescfmt += meminfo['module_type']
memdesc = memdescfmt.format(capacity, meminfo['speed'])
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
print('{0}: {1} manufacturer: {2}'.format(
node, prefix, meminfo['manufacturer']))
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
print('{0}: {1} serial number: {2}'.format(node, prefix,
meminfo['serial']))
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
exitcode = 0
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.exit(1)
try:
session = client.Command()
for res in session.read('/noderange/{0}/inventory/hardware/all/all'.format(
noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
if 'inventory' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
if not inv['present']:
print '{0}: {1}: Not Present'.format(node, prefix)
continue
info = inv['information']
info.pop('board_extra', None)
info.pop('oem_parser', None)
info.pop('chassis_extra', None)
info.pop('product_extra', None)
if 'memory_type' in info:
print_mem_info(node, prefix, info)
continue
for datum in info:
if info[datum] is None:
continue
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
pretty(datum),
info[datum]))
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2'
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
def attrrequested(attr, attrlist, seenattributes):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate == attr:
seenattributes.add(truename)
return True
elif '.' not in candidate and attr.startswith(candidate + '.'):
seenattributes.add(truename)
return True
return False
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 attrrequested(attr, args[1:], seenattributes):
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)
+11 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
@@ -25,8 +26,16 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange "
"([status|on|off|shutdown|boot|reset])")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
noderange = sys.argv[1]
setstate = None
if len(sys.argv) > 2:
if setstate == 'softoff':
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import select
import shlex
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog node commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
c = client.Command()
cmdstr = " ".join(args[1:])
nodeforpopen = {}
popens = []
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
ex = exp['databynode']
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
popens.append(nopen)
nodeforpopen[nopen] = node
all = set([])
pipedesc = {}
exitcode = 0
for pop in popens:
node = nodeforpopen[pop]
pipedesc[pop.stdout] = { 'node': node, 'popen': pop, 'type': 'stdout'}
pipedesc[pop.stderr] = {'node': node, 'popen': pop, 'type': 'stderr'}
all.add(pop.stdout)
all.add(pop.stderr)
rdy, _, _ = select.select(all, [], [], 10)
while all and rdy:
for r in rdy:
data = r.readline()
desc = pipedesc[r]
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
else:
sys.stderr.write('{0}: {1}'.format(node, data))
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
+26 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
# limitations under the License.
import csv
import datetime
import optparse
import os
import sys
@@ -39,13 +40,15 @@ sensorcollections = {
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [sensor(s)")
argparser.add_option('-i', '--interval', type='int',
usage="Usage: %prog [options] noderange ([sensor(s)])")
argparser.add_option('-i', '--interval', type='float',
help='Interval to do repeated samples over')
argparser.add_option('-n', '--numreadings', type='int',
help='Number of readings to gather')
argparser.add_option('-c', '--csv', action='store_true',
help='Output in CSV format')
argparser.add_option('-s', '--skipnumberless', action='store_true',
help='Output in CSV format')
(options, args) = argparser.parse_args()
repeatmode = False
if options.interval:
@@ -54,8 +57,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_help()
sys.exit(1)
sensors = []
for sensorgroup in args[1:]:
for sensor in sensorgroup.split(','):
@@ -98,6 +104,8 @@ def sensorpass(showout=True, appendtime=False):
if 'sensors' not in reading[node]:
continue
for sensedata in reading[node]['sensors']:
if sensedata['value'] is None and options.skipnumberless:
continue
for redundant_state in ('Non-Critical', 'Critical'):
try:
sensedata['states'].remove(redundant_state)
@@ -139,7 +147,12 @@ def sensorpass(showout=True, appendtime=False):
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
for nodekey in resdata:
if showtime:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
if showtime.is_integer():
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
else:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
str(datetime.datetime.now().microsecond//1000),
nodekey]
else:
rowdata = [nodekey]
for sensorkey in orderedsensors:
@@ -190,7 +203,8 @@ def main():
nextstart = os.times()[4] + options.interval
resdata = sensorpass(linebyline, True)
if options.csv:
format_csv(csvwriter, orderedsensors, resdata)
format_csv(csvwriter, orderedsensors, resdata,
showtime=options.interval)
if options.numreadings:
options.numreadings -= 1
if options.numreadings <= 0:
@@ -205,4 +219,8 @@ def main():
sensorpass(True)
main()
try:
main()
except KeyboardInterrupt:
print('')
sys.exit(0)
+26 -3
View File
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
@@ -25,12 +26,34 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='Usage: %prog [options] noderange [default|cd|network|setup|hd]')
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
noderange = sys.argv[1]
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
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(
session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev))
if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
+38 -20
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -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,
@@ -93,37 +97,50 @@ class Command(object):
return rc
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
key=None, **kwargs):
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
kwargs[ikey] = input
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
print('')
return 0
def read(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('retrieve', path, self.connection, parameters)
def update(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('update', path, self.connection, parameters)
def create(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('create', path, self.connection, parameters)
def delete(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('delete', path, self.connection, parameters)
def _connect_unix(self):
@@ -146,6 +163,7 @@ class Command(object):
try:
self.connection.settimeout(5)
self.connection.connect(sa)
self.connection.settimeout(None)
except:
raise
self.connection.close()
+20 -2
View File
@@ -16,18 +16,31 @@
# limitations under the License.
import confluent.tlv as tlv
from datetime import datetime
import json
import struct
def decodestr(value):
ret = None
try:
ret = value.decode('utf-8')
except UnicodeDecodeError:
try:
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], str):
dictdata[key] = dictdata[key].decode('utf-8')
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(dictdata[key], list):
for i in xrange(len(dictdata[key])):
if isinstance(dictdata[key][i], str):
dictdata[key][i] = dictdata[key][i].decode('utf-8')
dictdata[key][i] = decodestr(dictdata[key][i])
elif isinstance(dictdata[key][i], dict):
unicode_dictvalues(dictdata[key][i])
elif isinstance(dictdata[key], dict):
@@ -38,6 +51,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 +92,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
+37
View File
@@ -0,0 +1,37 @@
confetty(1) --- Interactive confluent client
=================================================
## SYNOPSIS
`confetty`
## DESCRIPTION
**confetty** launches an interactive CLI session to the
confluent service. It provides a filesystem-like
view of the confluent interface. It is intended to
be mostly an aid for developing client software, with
day to day administration generally being easier with
the various function specific commands.
## COMMANDS
The CLI may be navigated by shell commands and some other
commands.
* `cd`:
Change the location within the tree
* `ls`:
List the elements within the current directory/tree
* `show` **ELEMENT**, `cat` **ELEMENT**:
Display the result of reading a specific element (by full or relative path)
* `unset` **ELEMENT** **ATTRIBUTE**
For an element with attributes, request to clear the value of the attribue
* `set` **ELEMENT** **ATTRIBUTE**=**VALUE**
Set the specified attribute to the given value
* `start` **ELEMENT**
Start a console session indicated by **ELEMENT** (e.g. /nodes/n1/console/session)
* `rm` **ELEMENT**
Request removal of an element. (e.g. rm events/hardware/log clears log from a node)
+30
View File
@@ -0,0 +1,30 @@
nodeconsole(1) -- Open a console to a confluent node
=====================================================
## SYNOPSIS
`nodeconsole` `node`
## DESCRIPTION
**nodeconsole** opens an interactive console session to a given node. This is the
text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `c`,
then `.`. Note that console output by default is additionally logged to
`/var/log/confluent/consoles/`**NODENAME**.
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
sequences. To begin an command escape sequence, hit `Ctrl-e`, then `c`. The next
keystroke will be interpreted as a command. The following commands are available.
* `.`:
Exit the session and return to the command prompt
* `b`:
Send a break to the remote console when possible (some console plugins may not support this)
* `o`:
Request confluent to disconnect and reconnect to console. For example if there is suspicion
that the console has gone inoperable, but would work if reconnected.
* `?`:
Get a list of supported commands
* `<enter>`:
Abandon entering an escape sequence command
+48
View File
@@ -0,0 +1,48 @@
nodelist(1) -- List confluent nodes and their attributes
=========================================================
## SYNOPSIS
`nodelist` `noderange`
`nodelist` `noderange` [-b] [<nodeattribute>...]
## DESCRIPTION
**nodelist** queries the confluent server to get information about nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. There is more
information on node attributes in nodeattributes(5) man page.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodelist n1-n4`
`n1`
`n2`
`n3`
`n4`
* Getting an attribute of nodes matching a noderange:
`# nodelist n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodelist n1,n2 hardwaremanegement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: hardwaremanagement.switch: r8e1`
`n1: hardwaremanagement.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: hardwaremanagement.switch: r8e1`
`n2: hardwaremanagement.switchport: 2`
+55
View File
@@ -0,0 +1,55 @@
nodesensors(1) --- Retrieve telemetry for sensors of confluent nodes
====================================================================
## SYNOPSIS
`nodesensors` `noderange` [-c] [-i <interval>] [-n <samplecount>] [<sensor name or category>...]
## DESCRIPTION
**nodesensors** queries the confluent server to get telemetry from nodes. Telemetry can include
data such as temperature, power, and so forth. Without arguments, it lists all available sensors
and their current values. If `-c` is specified, CSV format is used for output. Normally
nodesensors outputs once and exits. Repeated periodic gathering can be done with `-i` to specify
interval and `-n` to specify number of requests. If `-i` is specified without `-n`, then it will
retrieve data at the requested interval indefinitely. If '-n' is specified without `-i`, an
interval of 1 second is used.
## OPTIONS
* `-c`, `--csv`:
Organize output into CSV format, one sensor per column.
* `-i`, `--interval`=**SECONDS**:
Repeat data gathering waiting, waiting the specified time between samples. Unless `-n` is
specified, indefinite retrieval is assumed.
* `-n`, `--numreadings`=**SAMPLES**:
Perform the specified number of readings, waiting `-i` indicated interval or 1 second if not
otherwise indicated.
## EXAMPLES
* Retrieving all temperature related sensors from one system
`# nodesensors n1 temperature`
`n1: CPU 1 Overtemp: Ok`
`n1: CPU 2 Overtemp: Ok`
`n1: Inlet Temp: 16.0 °C`
`n1: PCH Overtemp: Ok`
`n1: LOM Temp: Ok`
* Retrieving a sensor named "Inlet Temp" for 4 systems over a 3 second period of time:
`# nodesensors n1-n4 'Inlet Temp' -c -n 3`
`time,node,Inlet Temp (°C)`
`2016-10-04T15:09:20,n1,19.0`
`2016-10-04T15:09:20,n2,18.0`
`2016-10-04T15:09:20,n3,18.0`
`2016-10-04T15:09:20,n4,17.0`
`2016-10-04T15:09:21,n1,19.0`
`2016-10-04T15:09:21,n2,18.0`
`2016-10-04T15:09:21,n3,18.0`
`2016-10-04T15:09:21,n4,17.0`
`2016-10-04T15:09:22,n1,19.0`
`2016-10-04T15:09:22,n2,18.0`
`2016-10-04T15:09:22,n3,18.0`
`2016-10-04T15:09:22,n4,17.0`
+5 -1
View File
@@ -7,5 +7,9 @@ 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/nodefirmware', 'bin/nodehealth', 'bin/nodeidentify',
'bin/nodeinventory', 'bin/nodelist', 'bin/nodepower',
'bin/nodesensors', 'bin/nodesetboot', 'bin/noderun'],
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)
+187
View File
@@ -0,0 +1,187 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 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.
# Overall, the result of this shall be:
# - Web clients can create the same out-of-order responsiveness as socket
# clients (but with more complexity on their end)
# - Web clients can share single request among console sessions
# - Web clients can get async notify of things like node add/remove, events
# This provides an async strategy to http clients. The design is that a http
# session may have an 'async' resource. In such a case, any requests are
# queued and immediately the response is given accepting the queued request.
# A request flags itself as queue-compatible through an HTTP header indicating
# the identifier of the async thread. As responses happen to the queued
# request, data is dispatched to the first registered poller for data on
# the session. This way, a client may elect to provide multiple pollers
# to mitigate general choppiness of http network pattern. It may not be
# worth it, but it's possible.
# Additionally support console session multiplexing, to mitigate needed
# connection count.
# Also, this should allow a client to register for notifications of things
# like node add/delete or an event firing, ultimately.
# Much like console sessions, these will be reaped if a client spends too
# far away.
import collections
import confluent.exceptions as exc
import confluent.messages as messages
import confluent.util as util
import eventlet
import greenlet
import time
_asyncsessions = {}
_cleanthread = None
_consolesessions = None
def _assign_asyncid(asyncsession):
sessid = util.randomstring(32)
while sessid in _asyncsessions:
sessid = util.randomstring(32)
_asyncsessions[sessid] = {'asyncsession': asyncsession}
return sessid
class AsyncTermRelation(object):
# Need to keep an association of term object to async
# This allows the async handler to know the context of
# outgoing data to provide to calling code
def __init__(self, termid, async):
self.async = async
self.termid = termid
def got_data(self, data):
self.async.add(self.termid, data)
class AsyncSession(object):
def __init__(self):
self.asyncid = _assign_asyncid(self)
self.responses = collections.deque()
self._evt = None
self.termrelations = []
self.consoles = set([])
self.reaper = eventlet.spawn_after(15, self.destroy)
def add(self, requestid, rsp):
self.responses.append((requestid, rsp))
if self._evt:
self._evt.send()
self._evt = None
def set_term_relation(self, env):
# need a term relation to keep track of what data belongs
# to what object (since the callback does not provide context
# for data, and here ultimately the client is responsible
# for sorting out which is which.
termrel = AsyncTermRelation(env['HTTP_CONFLUENTREQUESTID'], self)
self.termrelations.append(termrel)
return termrel
def add_console_session(self, sessionid):
self.consoles.add(sessionid)
def destroy(self):
if self._evt:
self._evt.send()
self._evt = None
for console in self.consoles:
_consolesessions[console]['session'].destroy()
self.consoles = None
del _asyncsessions[self.asyncid]
def run_handler(self, handler, requestid):
try:
for rsp in handler:
self.add(requestid, rsp)
self.add(requestid, messages.AsyncCompletion())
except Exception as e:
self.add(requestid, e)
def get_responses(self, timeout=25):
self.reaper.cancel()
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
nextexpiry = time.time() + 90
for csess in list(self.consoles):
try:
_consolesessions[csess]['expiry'] = nextexpiry
except KeyError: # session has been closed elsewhere
self.consoles.discard(csess)
if self._evt:
# TODO(jjohnson2): This precludes the goal of 'double barreled'
# access.... revisit if this could matter
raise Exception('get_responses is not re-entrant')
if not self.responses: # wait to accumulate some
self._evt = eventlet.event.Event()
with eventlet.Timeout(timeout, False):
self._evt.wait()
self._evt = None
while self.responses:
yield self.responses.popleft()
def run_handler(hdlr, env):
asyncsessid = env['HTTP_CONFLUENTASYNCID']
try:
asyncsession = _asyncsessions[asyncsessid]['asyncsession']
requestid = env['HTTP_CONFLUENTREQUESTID']
except KeyError:
raise exc.InvalidArgumentException(
'Invalid Session ID or missing request id')
eventlet.spawn_n(asyncsession.run_handler, hdlr, requestid)
return requestid
def get_async(env, querydict):
global _cleanthread
return _asyncsessions[env['HTTP_CONFLUENTASYNCID']]['asyncsession']
def handle_async(env, querydict, threadset):
global _cleanthread
# This may be one of two things, a request for a new async stream
# or a request for next data from async stream
# httpapi otherwise handles requests an injecting them to queue
if 'asyncid' not in querydict or not querydict['asyncid']:
# This is a new request, create a new multiplexer
currsess = AsyncSession()
yield messages.AsyncSession(currsess.asyncid)
return
mythreadid = greenlet.getcurrent()
threadset.add(mythreadid)
loggedout = None
currsess = None
try:
currsess = _asyncsessions[querydict['asyncid']]['asyncsession']
for rsp in currsess.get_responses():
yield messages.AsyncMessage(rsp)
except greenlet.GreenletExit as ge:
loggedout = ge
threadset.discard(mythreadid)
if loggedout is not None:
currsess.destroy()
raise exc.LoggedOut()
def set_console_sessions(consolesessions):
global _consolesessions
_consolesessions = consolesessions
+8 -3
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'
@@ -113,7 +116,7 @@ def authorize(name, element, tenant=False, operation='create',
user, tenant = _get_usertenant(name, tenant)
if tenant is not None and not configmanager.is_tenant(tenant):
return None
manager = configmanager.ConfigManager(tenant)
manager = configmanager.ConfigManager(tenant, username=user)
if skipuserobj:
return None, manager, user, tenant, skipuserobj
userobj = manager.get_user(user)
@@ -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
@@ -173,7 +178,7 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# invalidate cache and force the slower check
del _passcache[(user, tenant)]
return None
cfm = configmanager.ConfigManager(tenant)
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None or 'cryptpass' not in ucfg:
eventlet.sleep(0.05) # stall even on test for existence of a username
+73 -43
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -80,6 +81,18 @@ node = {
#'id': {
# 'description': ('Numeric identifier for node')
#},
'location.room': {
'description': 'Room description for the node',
},
'location.row': {
'description': 'Row description for the rack the node is in',
},
'location.rack': {
'description': 'Rack number of the rack the node is in',
},
'location.u': {
'description': 'Position in the rack of the node',
},
# 'location.timezone': {
# 'description': 'POSIX timezone to apply to this node',
# },
@@ -175,54 +188,70 @@ node = {
# 'appliesto': ['vm'],
# },
'hardwaremanagement.manager': {
'description': 'The management address dedicated to this node',
'description': 'The management address dedicated to this node. This '
'is the address of, for example, the Lenovo IMM.',
},
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
},
# 'enclosure.manager': {
# 'description': "The management device for this node's chassis",
'hardwaremanagement.switch': {
'description': 'The switch to which the hardware manager is connected.'
' Only relevant if using switch based discovery via the'
' hardware manager (Lenovo IMMs and CMMs). Not '
'applicable to Lenovo Flex nodes.'
},
'hardwaremanagement.switchport': {
'description': 'The port of the switch that the hardware manager is '
'connected. See documentation of '
'hardwaremanagement.switch for more detail.'
},
'enclosure.manager': {
'description': "The management device for this node's chassis",
# 'appliesto': ['system'],
# },
# 'enclosure.bay': {
# 'description': 'The bay in the enclosure, if any',
},
'enclosure.bay': {
'description': 'The bay in the enclosure, if any',
# 'appliesto': ['system'],
# },
},
# 'enclosure.type': {
# 'description': '''The type of enclosure in use (e.g. IBM BladeCenter,
#IBM Flex)''',
# 'appliesto': ['system'],
# },
# 'inventory.serialnumber': {
# 'id.serial': {
# 'description': 'The manufacturer serial number of node',
# },
# 'inventory.uuid': {
# 'id.uuid': {
# 'description': 'The UUID of the node as presented in DMI',
# },
# 'inventory.modelnumber': {
# 'id.modelnumber': {
# 'description': 'The manufacturer dictated model number for the node',
# },
# 'inventory.snmpengineid': {
# 'id.modelname': {
# 'description': 'The manufacturer model label for the node',
# },
# 'id.snmpengineid': {
# 'description': 'The SNMP Engine id used by this node',
# },
# 'secret.snmpuser': {
# 'description': 'The user to use for SNMPv3 access to this node',
# },
# 'secret.snmppassphrase': {
# 'description': 'The passphrase to use for SNMPv3 access to this node',
# 'secret.snmppassword': {
# 'description': 'The password to use for SNMPv3 access to this node',
# },
'secret.snmpcommunity': {
'description': ('SNMPv1 community string, it is highly recommended to'
'step up to SNMPv3'),
},
# 'secret.snmplocalizedkey': {
# 'description': ("SNMPv3 key localized to this node's SNMP Engine id"
# 'This can be used in lieu of snmppassphrase to avoid'
# 'retaining the passphrase TODO: document procedure'
# 'to commit passphrase to localized key'),
# },
# 'secret.snmpcommunity': {
# 'description': ('SNMPv1 community string, it is highly recommended to'
# 'step up to SNMPv3'),
# },
# 'secret.localadminpassphrase': {
# 'secret.adminpassword': {
# 'description': ('The passphrase to apply to local root/administrator '
# 'account. '
# 'If the environment is 100% Linux, the value may be '
@@ -233,34 +262,35 @@ node = {
# 'AD')
# },
'secret.ipmikg': {
'description': 'Optional Integrity key for IPMI communication'
'description': 'Optional Integrity key for IPMI communication. This '
'should generally be ignored, as mutual authentication '
'is normally done with the password alone (which is a '
'shared secret in IPMI)'
},
# 'secret.ipmiuser': {
# 'description': ('The username to use to log into IPMI device related '
# 'to the node. For setting username, default '
# 'behavior is to randomize username, for using '
# 'username if not set, USERID is assumed'),
# },
# 'secret.ipmipassphrase': {
# 'description': ('The key to use to authenticate to IPMI device '
# 'related to the node. For setting passphrase, '
# 'default behavior is to randomize passphrase and '
# 'store it here. If going to connect over the '
# 'network and value is not set, PASSW0RD is attempted')
# },
'secret.hardwaremanagementuser': {
'description': ('Username to be set and used by protocols like SSH '
'and HTTP where client provides passphrase over the '
'network. Given the distinct security models betwen '
'this class of protocols and SNMP and IPMI, snmp and '
'ipmi utilize dedicated values.'),
'description': ('The username to use when connecting to the hardware '
'manager'),
},
'secret.hardwaremanagementpassword': {
'description': ('Passphrase to be set and used by protocols like SSH '
'and HTTP, where client sends passphrase over the '
'network. Given distinct security models between '
'this class of protocols, SNMP, and IPMI, SNMP and '
'IPMI are given their own settings with distinct '
'behaviors'),
'description': ('Password to use when connecting to the hardware '
'manager'),
},
'pubkeys.addpolicy': {
'description': ('Policy to use when encountering unknown public '
'keys. Choices are "automatic" to accept and '
'store new key if no key known and "manual" '
'to always reject a new key, even if no key known'
'Note that if the trusted CA verifies the certificate,'
' that is accepted ignoring this policy. Default '
'policy is "automatic"'),
'valid_values': ('automatic', 'manual'),
},
'pubkeys.tls_hardwaremanager': {
'description': ('Fingerprint of the TLS certificate recognized as'
'belonging to the hardware manager of the server'),
},
'pubkeys.ssh': {
'description': ('Fingerprint of the SSH key of the OS running on the '
'system.'),
},
}
+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)
@@ -203,8 +214,14 @@ def _load_dict_from_dbm(dpath, tdb):
if elem not in currdict:
currdict[elem] = {}
currdict = currdict[elem]
for tk in dbe.iterkeys():
currdict[tk] = cPickle.loads(dbe[tk])
try:
for tk in dbe.iterkeys():
currdict[tk] = cPickle.loads(dbe[tk])
except AttributeError:
tk = dbe.firstkey()
while tk != None:
currdict[tk] = cPickle.loads(dbe[tk])
tk = dbe.nextkey(tk)
except dbm.error:
return
@@ -405,7 +422,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
@@ -414,9 +435,10 @@ class ConfigManager(object):
_nodecollwatchers = {}
_notifierids = {}
def __init__(self, tenant, decrypt=False):
def __init__(self, tenant, decrypt=False, username=None):
global _cfgstore
self.decrypt = decrypt
self.current_user = username
if tenant is None:
self.tenant = None
if 'main' not in _cfgstore:
@@ -739,7 +761,17 @@ class ConfigManager(object):
decrypt=self.decrypt)
return nodeobj
def get_node_attributes(self, nodelist, attributes=()):
def expand_attrib_expression(self, nodelist, expression):
if type(nodelist) in (unicode, str):
nodelist = (nodelist,)
for node in nodelist:
cfgobj = self._cfgstore['nodes'][node]
fmt = _ExpressionFormat(cfgobj, node)
yield (node, fmt.format(expression))
def get_node_attributes(self, nodelist, attributes=(), decrypt=None):
if decrypt is None:
decrypt = self.decrypt
retdict = {}
relattribs = attributes
if isinstance(nodelist, str) or isinstance(nodelist, unicode):
@@ -761,7 +793,7 @@ class ConfigManager(object):
# skipped. The decryption, however, we want to do only on
# demand
nodeobj[attribute] = _decode_attribute(attribute, cfgnodeobj,
decrypt=self.decrypt)
decrypt=decrypt)
retdict[node] = nodeobj
return retdict
@@ -864,6 +896,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 +1008,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 +1050,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 +1117,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,23 +1241,27 @@ 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
@classmethod
def shutdown(cls):
def wait_for_sync(cls):
cls._bg_sync_to_file()
if cls._cfgwriter is not None:
cls._cfgwriter.join()
@classmethod
def shutdown(cls):
cls.wait_for_sync()
sys.exit(0)
@classmethod
@@ -1231,7 +1284,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 +1305,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 +1363,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))
+149 -122
View File
@@ -18,8 +18,8 @@
# whatever filehandle is conversing with the client and starts
# relaying data. It uses Ctrl-] like telnet for escape back to prompt
#we track nodes that are actively being logged, watched, or have attached
#there should be no more than one handler per node
# we track nodes that are actively being logged, watched, or have attached
# there should be no more than one handler per node
import collections
import confluent.config.configmanager as configmodule
import confluent.exceptions as exc
@@ -35,26 +35,30 @@ import traceback
_handled_consoles = {}
_genwatchattribs = frozenset(('console.method', 'console.logging'))
_tracelog = None
class _ConsoleHandler(object):
class ConsoleHandler(object):
_plugin_path = '/nodes/{0}/_console/session'
_logtobuffer = True
_genwatchattribs = frozenset(('console.method', 'console.logging'))
def __init__(self, node, configmanager):
self._dologging = True
self._isondemand = False
self.error = None
self.rcpts = {}
self.cfgmgr = configmanager
self.node = node
self.connectstate = 'unconnected'
self.clientcount = 0
self._isalive = True
self.logger = log.Logger(node, console=True,
tenant=configmanager.tenant)
self.buffer = bytearray()
(text, termstate, timestamp) = self.logger.read_recent_text(8192)
self.livesessions = set([])
if self._logtobuffer:
self.logger = log.Logger(node, console=True,
tenant=configmanager.tenant)
(text, termstate, timestamp) = self.logger.read_recent_text(8192)
else:
(text, termstate, timestamp) = ('', 0, False)
# when reading from log file, we will use wall clock
# it should usually match walltime.
self.lasttime = 0
@@ -79,10 +83,12 @@ class _ConsoleHandler(object):
self._console = None
self.connectionthread = None
self.send_break = None
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), _genwatchattribs, self._attribschanged)
if self._genwatchattribs:
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), self._genwatchattribs, self._attribschanged)
self.check_isondemand()
if not self._isondemand:
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
def check_isondemand(self):
@@ -134,7 +140,7 @@ class _ConsoleHandler(object):
self._ondemand()
if logvalue == 'none':
self._dologging = False
if not self._isondemand or self.clientcount > 0:
if not self._isondemand or self.livesessions:
eventlet.spawn(self._connect)
def log(self, *args, **kwargs):
@@ -166,7 +172,7 @@ class _ConsoleHandler(object):
def _ondemand(self):
self._isondemand = True
if self.clientcount < 1 and self._console:
if not self.livesessions and self._console:
self._disconnect()
def _connect(self):
@@ -186,8 +192,10 @@ class _ConsoleHandler(object):
self.reconnect = None
try:
self._console = plugin.handle_path(
"/nodes/%s/_console/session" % self.node,
self._plugin_path.format(self.node),
"create", self.cfgmgr)
except exc.NotImplementedException:
self._console = None
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
@@ -202,11 +210,12 @@ class _ConsoleHandler(object):
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
if hasattr(self._console, "configattributes"):
attribstowatch = self._console.configattributes | _genwatchattribs
attribstowatch = self._console.configattributes | self._genwatchattribs
else:
attribstowatch = _genwatchattribs
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
attribstowatch = self._genwatchattribs
if self._genwatchattribs:
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
try:
self._console.connect(self.get_console_output)
except exc.TargetEndpointBadCredentials:
@@ -214,7 +223,7 @@ class _ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -223,7 +232,7 @@ class _ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -234,7 +243,7 @@ class _ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -248,11 +257,12 @@ class _ConsoleHandler(object):
self._send_rcpts({'connectstate': self.connectstate})
def _got_disconnected(self):
self.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._send_rcpts({'connectstate': self.connectstate})
if self.connectstate != 'unconnected':
self.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._send_rcpts({'connectstate': self.connectstate})
if self._isalive:
self._connect()
@@ -268,32 +278,6 @@ class _ConsoleHandler(object):
self.connectionthread.kill()
self.connectionthread = None
def unregister_rcpt(self, handle):
self.clientcount -= 1
if handle in self.rcpts:
del self.rcpts[handle]
self._send_rcpts({'clientcount': self.clientcount})
if self._isondemand and self.clientcount < 1:
self._disconnect()
def register_rcpt(self, callback):
self.clientcount += 1
self._send_rcpts({'clientcount': self.clientcount})
hdl = random.random()
while hdl in self.rcpts:
hdl = random.random()
self.rcpts[hdl] = callback
if self.connectstate == 'unconnected':
# if console is not connected, take time to try to assert
# connectivity now.
if self.reconnect:
# cancel an automated retry if one is pending
self.reconnect.cancel()
self.reconnect = None
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
return hdl
def flushbuffer(self):
# Logging is handled in a different stream
# this buffer is now just for having screen redraw on
@@ -305,39 +289,44 @@ class _ConsoleHandler(object):
# to the console object
eventlet.spawn(self._handle_console_output, data)
def attachuser(self, username):
if username in self.users:
self.users[username] += 1
else:
self.users[username] = 1
edata = self.users[username]
if edata > 2: # for log purposes, only need to
# clearly indicate redundant connections
# not connection count
edata = 2
if edata < 0:
_tracelog.log('client count negative' + traceback.format_exc(),
ltype=log.DataTypes.event,
event=log.Events.stacktrace)
edata = 0
def attachsession(self, session):
edata = 1
for currsession in self.livesessions:
if currsession.username == session.username:
# indicate that user has multiple connections
edata = 2
self.livesessions.add(session)
self.log(
logdata=username, ltype=log.DataTypes.event,
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
if self.connectstate == 'unconnected':
# if console is not connected, take time to try to assert
# connectivity now.
if self.reconnect:
# cancel an automated retry if one is pending
self.reconnect.cancel()
self.reconnect = None
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
def detachuser(self, username):
self.users[username] -= 1
if self.users[username] < 2:
edata = self.users[username]
else:
edata = 2
if edata < 0:
_tracelog.log('client count negative' + traceback.format_exc(),
ltype=log.DataTypes.event,
event=log.Events.stacktrace)
edata = 0
def detachsession(self, session):
edata = 0
self.livesessions.discard(session)
for currsession in self.livesessions:
if currsession.username == session.username:
edata += 1
if edata > 1: # don't bother counting beyond 2 in the log
break
self.log(
logdata=username, ltype=log.DataTypes.event,
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientdisconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
if self._isondemand and not self.livesessions:
self._disconnect()
def reopen(self):
self._got_disconnected()
@@ -347,6 +336,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,8 +354,11 @@ class _ConsoleHandler(object):
eventdata |= 2
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.buffer += data
#TODO: analyze buffer for registered events, examples:
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
if len(self.buffer) > 16384:
@@ -371,39 +366,40 @@ class _ConsoleHandler(object):
self._send_rcpts(data)
def _send_rcpts(self, data):
for rcpt in self.rcpts.itervalues():
for rcpt in self.livesessions:
try:
rcpt(data)
rcpt.data_handler(data)
except: # No matter the reason, advance to next recipient
pass
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def get_recent(self):
"""Retrieve 'recent' data
Replay data in the intent to perhaps reproduce the display.
"""
#For now, just try to seek back in buffer to find a clear screen
#If that fails, just return buffer
#a scheme always tracking the last clear screen would be too costly
# For now, just try to seek back in buffer to find a clear screen
# If that fails, just return buffer
# a scheme always tracking the last clear screen would be too costly
connstate = {
'connectstate': self.connectstate,
'clientcount': self.clientcount,
'clientcount': len(self.livesessions),
}
retdata = ''
if self.shiftin is not None: # detected that terminal requested a
#shiftin character set, relay that to the terminal that cannected
# shiftin character set, relay that to the terminal that cannected
retdata += '\x1b)' + self.shiftin
if self.appmodedetected:
retdata += '\x1b[?1h'
else:
retdata += '\x1b[?1l'
#an alternative would be to emulate a VT100 to know what the
#whole screen would look like
#this is one scheme to clear screen, move cursor then clear
# an alternative would be to emulate a VT100 to know what the
# whole screen would look like
# this is one scheme to clear screen, move cursor then clear
bufidx = self.buffer.rfind('\x1b[H\x1b[J')
if bufidx >= 0:
return retdata + str(self.buffer[bufidx:]), connstate
#another scheme is the 2J scheme
# another scheme is the 2J scheme
bufidx = self.buffer.rfind('\x1b[2J')
if bufidx >= 0:
# there was some sort of clear screen event
@@ -411,8 +407,8 @@ class _ConsoleHandler(object):
# in hopes that it reproduces the screen
return retdata + str(self.buffer[bufidx:]), connstate
else:
#we have no indication of last erase, play back last kibibyte
#to give some sense of context anyway
# we have no indication of last erase, play back last kibibyte
# to give some sense of context anyway
return retdata + str(self.buffer[-1024:]), connstate
def write(self, data):
@@ -450,13 +446,15 @@ def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager):
def connect_node(node, configmanager, username=None):
consk = (node, configmanager.tenant)
if consk not in _handled_consoles:
_handled_consoles[consk] = _ConsoleHandler(node, configmanager)
#this represents some api view of a console handler. This handles things like
#holding the caller specific queue data, for example, when http api should be
#sending data, but there is no outstanding POST request to hold it,
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
return _handled_consoles[consk]
# this represents some api view of a console handler. This handles things like
# holding the caller specific queue data, for example, when http api should be
# sending data, but there is no outstanding POST request to hold it,
# this object has the job of holding the data
@@ -468,7 +466,14 @@ class ConsoleSession(object):
event watching will all be handled seamlessly
:param node: Name of the node for which this session will be created
:param configmanager: A configuration manager object for current context
:param username: Username for which this session object will operate
:param datacallback: An asynchronous data handler, to be called when data
is available. Note that if passed, it makes
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
connector = connect_node
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False):
@@ -476,30 +481,41 @@ class ConsoleSession(object):
self.tenant = configmanager.tenant
if not configmanager.is_node(node):
raise exc.NotFoundException("Invalid node")
consk = (node, self.tenant)
self.ckey = consk
self.username = username
connect_node(node, configmanager)
self.node = node
self.configmanager = configmanager
self.connect_session()
self.registered = True
_handled_consoles[consk].attachuser(username)
self._evt = None
self.node = node
self.conshdl = _handled_consoles[consk]
self.write = _handled_consoles[consk].write
self.write = self.conshdl.write
if datacallback is None:
self.reaper = eventlet.spawn_after(15, self.destroy)
self.databuffer = collections.deque([])
self.reghdl = _handled_consoles[consk].register_rcpt(self.got_data)
self.data_handler = self.got_data
if not skipreplay:
self.databuffer.extend(_handled_consoles[consk].get_recent())
self.databuffer.extend(self.conshdl.get_recent())
else:
self.reghdl = _handled_consoles[consk].register_rcpt(datacallback)
self.data_handler = datacallback
if not skipreplay:
for recdata in _handled_consoles[consk].get_recent():
for recdata in self.conshdl.get_recent():
if recdata:
datacallback(recdata)
self.conshdl.attachsession(self)
def connect_session(self):
"""Connect to the appropriate backend handler
This is not intended to be called by your usual consumer,
it is a hook for confluent to abstract the concept of a terminal
between console and shell.
"""
self.conshdl = connect_node(self.node, self.configmanager,
self.username)
def send_break(self):
"""Send break to remote system
"""
self.conshdl.send_break()
def get_buffer_age(self):
@@ -509,13 +525,21 @@ class ConsoleSession(object):
return self.conshdl.get_buffer_age()
def reopen(self):
"""Reopen the session
This can be useful if there is suspicion that the remote console is
dead. Note that developers should consider need for this a bug unless
there really is some fundamental, unavoidable limitation regarding
automatically detecting an unusable console in the underlying
technology that cannot be unambiguously autodetected.
"""
self.conshdl.reopen()
def destroy(self):
if self.registered:
_handled_consoles[self.ckey].detachuser(self.username)
_handled_consoles[self.ckey].unregister_rcpt(self.reghdl)
self.databuffer = None
self.conshdl.detachsession(self)
if self._evt:
self._evt.send()
self._evt = None
self.reghdl = None
@@ -523,19 +547,26 @@ class ConsoleSession(object):
"""Receive data from console and buffer
If the caller does not provide a callback and instead will be polling
for data, we must maintain data in a buffer until retrieved
for data, we must maintain data in a buffer until retrieved. This is
an internal function used as a means to convert the async behavior to
polling for consumers that cannot do the async behavior.
"""
self.databuffer.append(data)
if self._evt:
self._evt.send()
self._evt = None
def get_next_output(self, timeout=45):
"""Poll for next available output on this console.
Ideally purely event driven scheme is perfect. AJAX over HTTP is
at least one case where we don't have that luxury
at least one case where we don't have that luxury. This function
will not work if the session was initialized with a data callback
instead of polling mode.
"""
self.reaper.cancel()
# postpone death to be 15 seconds after this would timeout
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
if self._evt:
raise Exception('get_next_output is not re-entrant')
if not self.databuffer:
@@ -544,16 +575,12 @@ class ConsoleSession(object):
self._evt.wait()
self._evt = None
if not self.databuffer:
self.reaper = eventlet.spawn_after(15, self.destroy)
return ""
currdata = self.databuffer.popleft()
if isinstance(currdata, dict):
self.reaper = eventlet.spawn_after(15, self.destroy)
return currdata
retval = currdata
while self.databuffer and not isinstance(self.databuffer[0], dict):
retval += self.databuffer.popleft()
# the client has 15 seconds to make a new request for data before
# they are given up on
self.reaper = eventlet.spawn_after(15, self.destroy)
return retval
+218 -86
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,15 +60,17 @@ 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")
def load_plugins():
# To know our plugins directory, we get the parent path of 'bin'
_init_core()
path = os.path.dirname(os.path.realpath(__file__))
plugintop = os.path.realpath(os.path.join(path, 'plugins'))
plugins = set()
@@ -71,8 +78,8 @@ def load_plugins():
plugindir = os.path.join(plugintop, plugindir)
if not os.path.isdir(plugindir):
continue
sys.path.append(plugindir)
#two passes, to avoid adding both py and pyc files
sys.path.insert(1, plugindir)
# two passes, to avoid adding both py and pyc files
for plugin in os.listdir(plugindir):
if plugin.startswith('.'):
continue
@@ -80,7 +87,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)
@@ -89,85 +96,180 @@ def load_plugins():
pluginmap[name] = tmpmod
else:
pluginmap[plugin] = tmpmod
# restore path to not include the plugindir
sys.path.pop(1)
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
# _ prefix indicates internal use (e.g. special console scheme) and should not
# be enumerated in any collection
noderesources = {
'_console': {
'session': PluginRoute({
'pluginattrs': ['console.method'],
}),
},
'console': {
#this is a dummy value, http or socket must handle special
'session': PluginRoute({}),
},
'power': {
'state': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'identify': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'boot': {
'nextdevice': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
'sensors': {
'hardware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'power': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'fans': PluginCollection({
def _init_core():
global noderesources
global nodegroupresources
import confluent.shellserver as shellserver
# _ 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'}),
'expression': 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'],
}),
},
'_shell': {
'session': PluginRoute({
# For now, not configurable, wait until there's demand
'handler': 'ssh',
}),
},
'shell': {
# another special case similar to console
'sessions': PluginCollection({
'handler': shellserver,
}),
},
'console': {
# this is a dummy value, http or socket must handle special
'session': None,
'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'],
'default': 'ipmi',
}),
},
'identify': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'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',
}),
},
'sensors': {
'hardware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'power': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'fans': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'leds': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
nodegroupresources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
}
},
}
nodegroupresources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
}
def create_user(inputdata, configmanager):
@@ -313,6 +415,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 +440,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 +459,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 +500,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:
@@ -416,6 +520,8 @@ def handle_node_request(configmanager, inputdata, operation,
iscollection = True
elif isinstance(routespec, PluginCollection):
iscollection = False # it is a collection, but plugin defined
elif routespec is None:
raise exc.InvalidArgumentException('Custom interface required for resource')
if iscollection:
if operation == "delete":
return delete_node_collection(pathcomponents, configmanager)
@@ -427,15 +533,20 @@ 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(
if isinstance(plugroute['handler'], str):
hfunc = getattr(pluginmap[plugroute['handler']], operation)
else:
hfunc = getattr(plugroute['handler'], operation)
passvalue = hfunc(
nodes=nodes, element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata)
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
return passvalue
else:
return stripnode(passvalue, nodes[0])
elif 'pluginattrs' in plugroute:
@@ -455,19 +566,29 @@ 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])
if len(passvalues) > 0:
if isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
else:
raise exc.NotImplementedException()
def handle_path(path, operation, configmanager, inputdata=None):
# elif isinstance(passvalues[0], console.Console):
# return passvalues[0]
# else:
# return stripnode(passvalues[0], nodes[0])
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 +603,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 +635,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()
+59 -7
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,41 +15,92 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import json
class ConfluentException(Exception):
pass
apierrorcode = 500
apierrorstr = 'Unexpected Error'
def get_error_body(self):
errstr = ' - '.join((self.apierrorstr, str(self)))
return json.dumps({'error': errstr })
class NotFoundException(ConfluentException):
# Something that could be construed as a name was not found
# basically, picture an http error code 404
pass
apierrorcode = 404
apierrorstr = 'Request path not recognized'
class InvalidArgumentException(ConfluentException):
# Something from the remote client wasn't correct
# like http code 400
pass
apierrorcode = 400
apierrorstr = 'Bad Request'
class TargetEndpointUnreachable(ConfluentException):
# A target system was unavailable. For example, a BMC
# was unreachable. http code 504
pass
apierrorcode = 504
apierrorstr = 'Unreachable Target'
class TargetEndpointBadCredentials(ConfluentException):
# target was reachable, but authentication/authorization
# failed
pass
apierrorcode = 502
apierrorstr = 'Bad Credentials'
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
apierrorstr = 'Credential store locked'
class ForbiddenRequest(ConfluentException):
# The client request is not allowed by authorization engine
pass
apierrorcode = 403
apierrorstr = 'Forbidden'
class NotImplementedException(ConfluentException):
# The current configuration/plugin is unable to perform
# the requested task. http code 501
pass
apierrorcode = 501
apierrorstr = '501 - Not Implemented'
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
apierrorstr = 'Global configuration contains an error'
class PubkeyInvalid(ConfluentException):
apierrorcode = 502
apierrorstr = '502 - Invalid certificate or key on target'
def __init__(self, text, certificate, fingerprint, attribname, event):
super(PubkeyInvalid, self).__init__(self, text)
self.fingerprint = fingerprint
bodydata = {'message': text,
'event': event,
'fingerprint': fingerprint,
'fingerprintfield': attribname,
'certificate': base64.b64encode(certificate)}
self.errorbody = json.dumps(bodydata)
def get_error_body(self):
return self.errorbody
class LoggedOut(ConfluentException):
apierrorcode = 401
apierrorstr = '401 - Logged out'
def get_error_body(self):
return '{"loggedout": 1}'
+310 -72
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -25,22 +25,29 @@ 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.asynchttp
import confluent.shellserver as shellserver
import confluent.tlvdata
import confluent.util as util
import copy
import eventlet
import eventlet.greenthread
import greenlet
import json
import socket
import sys
import traceback
import time
import urlparse
import eventlet.wsgi
#scgi = eventlet.import_patched('flup.server.scgi')
tlvdata = confluent.tlvdata
auditlog = None
tracelog = None
consolesessions = {}
confluent.asynchttp.set_console_sessions(consolesessions)
httpsessions = {}
opmap = {
'POST': 'create',
@@ -103,21 +110,54 @@ 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,
}
def _sessioncleaner():
while True:
currtime = time.time()
for session in httpsessions.keys():
targsessions = []
for session in httpsessions:
if httpsessions[session]['expiry'] < currtime:
del httpsessions[session]
for session in consolesessions.keys():
targsessions.append(session)
for session in targsessions:
del httpsessions[session]
targsessions = []
for session in consolesessions:
if consolesessions[session]['expiry'] < currtime:
del consolesessions[session]
targsessions.append(session)
for session in targsessions:
del consolesessions[session]
eventlet.sleep(10)
@@ -155,6 +195,63 @@ def _get_query_dict(env, reqbody, reqtype):
qdict = nqdict
return qdict
def _should_skip_authlog(env):
if ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO']):
# we should only log starting of a console
return True
if '/sessions/current/async' in env['PATH_INFO']:
# this is effectively invisible
return True
if (env['REQUEST_METHOD'] == 'GET' and
('/sensors/' in env['PATH_INFO'] or
'/health/' in env['PATH_INFO'] or
'/power/state' in env['PATH_INFO'] or
'/nodes/' == env['PATH_INFO'] or
'/sessions/current/info' == env['PATH_INFO'] or
(env['PATH_INFO'].startswith('/noderange/') and
env['PATH_INFO'].endswith('/nodes/')))):
# these are pretty innocuous, and noisy to log.
return True
return False
def _csrf_valid(env, session):
# This could be simplified into a statement, but this is more readable
# to have it broken out
if (env['REQUEST_METHOD'] == 'GET' and
env['PATH_INFO'] == '/sessions/current/info'):
# Provide a web client a safe hook to request the CSRF token
# This means that we consider GET of /sessions/current/info to be
# a safe thing to inflict via CSRF, since CORS should prevent
# hypothetical attacker from reading the data and it has no
# side effects to speak of
return True
if 'csrftoken' not in session:
# The client has not (yet) requested CSRF protection
# so we return true
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
# The client has requested CSRF countermeasures,
# oblige the request and apply a new token to the
# session
session['csrftoken'] = util.randomstring(32)
elif 'HTTP_REFERER' in env:
# If there is a referrer, make sure it stays consistent
# across the session. A change in referer is a bad thing
try:
referer = env['HTTP_REFERER'].split('/')[2]
except IndexError:
return False
if 'validreferer' not in session:
session['validreferer'] = referer
elif session['validreferer'] != referer:
return False
return True
# The session has CSRF protection enabled, only mark valid if
# the client has provided an auth token and that token matches the
# value protecting the session
return ('HTTP_CONFLUENTAUTHTOKEN' in env and
env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken'])
def _authorize_request(env, operation):
"""Grant/Deny access based on data from wsgi env
@@ -162,6 +259,7 @@ def _authorize_request(env, operation):
"""
authdata = None
name = ''
sessionid = None
cookie = Cookie.SimpleCookie()
if 'HTTP_COOKIE' in env:
#attempt to use the cookie. If it matches
@@ -169,13 +267,31 @@ def _authorize_request(env, operation):
cc.load(env['HTTP_COOKIE'])
if 'confluentsessionid' in cc:
sessionid = cc['confluentsessionid'].value
sessid = sessionid
if sessionid in httpsessions:
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if _csrf_valid(env, httpsessions[sessionid]):
if env['PATH_INFO'] == '/sessions/current/logout':
targets = []
for mythread in httpsessions[sessionid]['inflight']:
targets.append(mythread)
for mythread in targets:
eventlet.greenthread.kill(mythread)
del httpsessions[sessionid]
return ('logout',)
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
if env['PATH_INFO'] == '/sessions/current/logout':
if 'HTTP_REFERER' in env:
# note that this doesn't actually do harm
# otherwise, but this way do not give appearance
# of something having a side effect if it has the smell
# of a CSRF
return {'code': 401}
return ('logout',)
name, passphrase = base64.b64decode(
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
authdata = auth.check_user_passphrase(name, passphrase, element=None)
@@ -185,14 +301,15 @@ def _authorize_request(env, operation):
while sessid in httpsessions:
sessid = util.randomstring(32)
httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90,
'skipuserobject': authdata[4]}
'skipuserobject': authdata[4],
'inflight': set([])}
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
httpsessions[sessid]['csrftoken'] = util.randomstring(32)
cookie['confluentsessionid'] = sessid
cookie['confluentsessionid']['secure'] = 1
cookie['confluentsessionid']['httponly'] = 1
cookie['confluentsessionid']['path'] = '/'
skiplog = False
if '/console/session' in env['PATH_INFO']:
skiplog = True
skiplog = _should_skip_authlog(env)
if authdata:
auditmsg = {
'user': name,
@@ -208,8 +325,12 @@ def _authorize_request(env, operation):
auditmsg['tenant'] = authdata[3]
authinfo['tenant'] = authdata[3]
auditmsg['user'] = authdata[2]
if sessid is not None:
authinfo['sessionid'] = sessid
if not skiplog:
auditlog.log(auditmsg)
if 'csrftoken' in httpsessions[sessid]:
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
return authinfo
else:
return {'code': 401}
@@ -235,7 +356,7 @@ def _pick_mimetype(env):
return 'application/json; charset=utf-8', '.json'
elif env['PATH_INFO'].endswith('.html'):
return 'text/html', '.html'
elif 'application/json' in env['HTTP_ACCEPT']:
elif 'HTTP_ACCEPT' in env and 'application/json' in env['HTTP_ACCEPT']:
return 'application/json; charset=utf-8', ''
else:
return 'text/html', ''
@@ -243,7 +364,7 @@ def _pick_mimetype(env):
def _assign_consessionid(consolesession):
sessid = util.randomstring(32)
while sessid in consolesessions.keys():
while sessid in consolesessions:
sessid = util.randomstring(32)
consolesessions[sessid] = {'session': consolesession,
'expiry': time.time() + 60}
@@ -266,6 +387,13 @@ def resourcehandler_backend(env, start_response):
"""Function to handle new wsgi requests
"""
mimetype, extension = _pick_mimetype(env)
headers = [('Content-Type', mimetype), ('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
('X-Content-Type-Options', 'nosniff'),
('Content-Security-Policy', "default-src 'self'"),
('X-XSS-Protection', '1'), ('X-Frame-Options', 'deny'),
('Strict-Transport-Security', 'max-age=86400'),
('X-Permitted-Cross-Domain-Policies', 'none')]
reqbody = None
reqtype = None
if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
@@ -277,30 +405,56 @@ def resourcehandler_backend(env, start_response):
operation = querydict['restexplorerop']
del querydict['restexplorerop']
authorized = _authorize_request(env, operation)
if 'logout' in authorized:
start_response('200 Successful logout', headers)
yield('{"result": "200 - Successful logout"}')
return
if 'HTTP_SUPPRESSAUTHHEADER' in env or 'HTTP_CONFLUENTAUTHTOKEN' in env:
badauth = [('Content-type', 'text/plain')]
else:
badauth = [('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')]
if authorized['code'] == 401:
start_response(
'401 Authentication Required',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
start_response('401 Authentication Required', badauth)
yield 'authentication required'
return
if authorized['code'] == 403:
start_response(
'403 Forbidden',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
start_response('403 Forbidden', badauth)
yield 'authorization failed'
return
if authorized['code'] != 200:
raise Exception("Unrecognized code from auth engine")
headers = [('Content-Type', mimetype)]
headers.extend(
("Set-Cookie", m.OutputString())
for m in authorized['cookie'].values())
cfgmgr = authorized['cfgmgr']
if '/console/session' in env['PATH_INFO']:
if (operation == 'create') and env['PATH_INFO'] == '/sessions/current/async':
pagecontent = ""
try:
for rsp in _assemble_json(
confluent.asynchttp.handle_async(
env, querydict,
httpsessions[authorized['sessionid']]['inflight'])):
pagecontent += rsp
start_response("200 OK", headers)
yield pagecontent
return
except exc.ConfluentException as e:
if e.apierrorcode == 500:
# raise generics to trigger the tracelog
raise
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
headers)
yield e.get_error_body()
elif (operation == 'create' and ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO'])):
#hard bake JSON into this path, do not support other incarnations
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
if '/console/session' in env['PATH_INFO']:
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
shellsession = False
elif '/shell/sessions/' in env['PATH_INFO']:
prefix, _, _ = env['PATH_INFO'].partition('/shell/sessions')
shellsession = True
_, _, nodename = prefix.rpartition('/')
if 'session' not in querydict.keys() or not querydict['session']:
auditmsg = {
@@ -315,10 +469,25 @@ def resourcehandler_backend(env, start_response):
skipreplay = False
if 'skipreplay' in querydict and querydict['skipreplay']:
skipreplay = True
datacallback = None
async = None
if 'HTTP_CONFLUENTASYNCID' in env:
async = confluent.asynchttp.get_async(env, querydict)
termrel = async.set_term_relation(env)
datacallback = termrel.got_data
try:
consession = consoleserver.ConsoleSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay)
if shellsession:
consession = shellserver.ShellSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
)
else:
consession = consoleserver.ConsoleSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
)
except exc.NotFoundException:
start_response("404 Not found", headers)
yield "404 - Request Path not recognized"
@@ -327,6 +496,8 @@ def resourcehandler_backend(env, start_response):
start_response("500 Internal Server Error", headers)
return
sessid = _assign_consessionid(consession)
if async:
async.add_console_session(sessid)
start_response('200 OK', headers)
yield '{"session":"%s","data":""}' % sessid
return
@@ -341,14 +512,51 @@ def resourcehandler_backend(env, start_response):
start_response('200 OK', headers)
yield json.dumps({'session': querydict['session']})
return # client has requests to send or receive, not both...
elif 'closesession' in querydict:
consolesessions[querydict['session']]['session'].destroy()
del consolesessions[querydict['session']]
start_response('200 OK', headers)
yield '{"sessionclosed": true}'
return
elif 'action' in querydict:
if querydict['action'] == 'break':
consolesessions[querydict['session']]['session'].send_break()
elif querydict['action'] == 'reopen':
consolesessions[querydict['session']]['session'].reopen()
else:
start_response('400 Bad Request')
yield 'Unrecognized action ' + querydict['action']
return
start_response('200 OK', headers)
yield json.dumps({'session': querydict['session']})
else: # no keys, but a session, means it's hooking to receive data
sessid = querydict['session']
if sessid not in consolesessions:
start_response('400 Expired Session', headers)
yield ''
return
consolesessions[sessid]['expiry'] = time.time() + 90
outdata = consolesessions[sessid]['session'].get_next_output(
timeout=45)
# add our thread to the 'inflight' to have a hook to terminate
# a long polling request
loggedout = None
mythreadid = greenlet.getcurrent()
httpsessions[authorized['sessionid']]['inflight'].add(mythreadid)
try:
outdata = consolesessions[sessid]['session'].get_next_output(
timeout=25)
except greenlet.GreenletExit as ge:
loggedout = ge
httpsessions[authorized['sessionid']]['inflight'].discard(
mythreadid)
if sessid not in consolesessions:
start_response('400 Expired Session', headers)
yield ''
return
if loggedout is not None:
consolesessions[sessid]['session'].destroy()
start_response('401 Logged out', headers)
yield '{"loggedout": 1}'
return
bufferage = False
if 'stampsent' not in consolesessions[sessid]:
consolesessions[sessid]['stampsent'] = True
@@ -377,12 +585,23 @@ def resourcehandler_backend(env, start_response):
url = env['PATH_INFO']
url = url.replace('.json', '')
url = url.replace('.html', '')
if url == '/sessions/current/info':
start_response('200 OK', headers)
sessinfo = {'username': authorized['username']}
if 'authtoken' in authorized:
sessinfo['authtoken'] = authorized['authtoken']
yield json.dumps(sessinfo)
return
resource = '.' + url[url.rindex('/'):]
lquerydict = copy.deepcopy(querydict)
try:
hdlr = pluginapi.handle_path(url, operation,
cfgmgr, querydict)
if 'HTTP_CONFLUENTASYNCID' in env:
confluent.asynchttp.run_handler(hdlr, env)
start_response('202 Accepted', headers)
yield 'Request queued'
return
pagecontent = ""
if mimetype == 'text/html':
for datum in _assemble_html(hdlr, resource, lquerydict, url,
@@ -393,21 +612,14 @@ def resourcehandler_backend(env, start_response):
pagecontent += datum
start_response('200 OK', headers)
yield pagecontent
except exc.NotFoundException as ne:
start_response('404 Not found', headers)
yield "404 - Request path not recognized - " + str(ne)
except exc.InvalidArgumentException as e:
start_response('400 Bad Request - ' + str(e), headers)
yield '400 - Bad Request - ' + str(e)
except exc.TargetEndpointUnreachable:
start_response('504 Unreachable Target', headers)
yield '504 - Unreachable Target'
except exc.TargetEndpointBadCredentials:
start_response('502 Bad Credentials', headers)
yield '502 - Bad Credentials'
except exc.NotImplementedException:
start_response('501 Not Implemented', headers)
yield '501 Not Implemented'
except exc.ConfluentException as e:
if ((not isinstance(e, exc.LockedCredentials)) and
e.apierrorcode == 500):
# raise generics to trigger the tracelog
raise
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
headers)
yield e.get_error_body()
def _assemble_html(responses, resource, querydict, url, extension):
yield '<html><head><meta charset="UTF-8"><title>' \
@@ -429,7 +641,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 +656,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">'
@@ -461,33 +676,37 @@ def _assemble_html(responses, resource, querydict, url, extension):
'</form></body></html>')
def _assemble_json(responses, resource, url, extension):
def _assemble_json(responses, resource=None, url=None, extension=None):
#NOTE(jbjohnso) I'm considering giving up on yielding bit by bit
#in json case over http. Notably, duplicate key values from plugin
#overwrite, but we'd want to preserve them into an array instead.
#the downside is that http would just always blurt it ll out at
#once and hold on to all the data in memory
links = {
'self': {"href": resource + extension},
}
if url == '/':
pass
elif resource[-1] == '/':
links['collection'] = {"href": "../" + extension}
else:
links['collection'] = {"href": "./" + extension}
links = {}
if resource is not None:
links['self'] = {"href": resource + extension}
if url == '/':
pass
elif resource[-1] == '/':
links['collection'] = {"href": "../" + extension}
else:
links['collection'] = {"href": "./" + extension}
rspdata = {}
for rsp in responses:
if isinstance(rsp, confluent.messages.LinkRelation):
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 +718,20 @@ def _assemble_json(responses, resource, url, extension):
else:
rspdata[dk] = [rspdata[dk], rsp[dk]]
else:
rspdata[dk] = rsp[dk]
if dk == 'databynode' or dk == 'asyncresponse':
# 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
@@ -517,21 +742,34 @@ def serve():
#but deps are simpler without flup
#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),
resourcehandler, log=False, log_output=False, debug=False)
sock = None
while not sock:
try:
sock = eventlet.listen(
(bind_host, bind_port, 0, 0), family=socket.AF_INET6)
except socket.error as e:
if e.errno != 98:
raise
sys.stderr.write(
'Failed to open HTTP due to busy port, trying again in'
' a second\n')
eventlet.sleep(1)
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
debug=False)
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
+516 -42
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -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,376 @@ 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 0
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)
odbfn = dbfn
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
odtfn = dtfn
append=1
while os.path.exists(dbfn):
dbfn = odbfn + '.{}'.format(append)
append += 1
append=1
while os.path.exists(dtfn):
dtfn = odtfn + '.{}'.format(append)
append += 1
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 +512,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 +548,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,54 +567,123 @@ 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
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:
txtpath, bpath = parse_last_rolling_files(textfile, offset,
datalen)
if txtpath == textpath:
break
if bpath == binpath:
break
textpath = txtpath
binpath = bpath
# Rolling event detected, close the current bin file, then open
# the renamed bin file.
flock(binfile, LOCK_UN)
flock(textfile, LOCK_UN)
binfile.close()
textfile.close()
try:
binfile = open(binpath, mode='r')
textfile = open(textpath, mode='r')
except IOError:
binfile = None
textfile = None
break
flock(binfile, LOCK_SH)
flock(textfile, 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)
binfile.close()
try:
flock(binfile, LOCK_UN)
binfile.close()
except:
pass
textdata = ''
fcntl.flock(textfile, fcntl.LOCK_SH)
while offsets:
(offset, length) = offsets.pop()
textfile.seek(offset, 0)
(offset, length, textpath) = offsets.pop()
if textfile is None:
textfile = open(textpath, mode='r')
flock(textfile, LOCK_SH)
if textfile.name != textpath:
try:
flock(textfile, LOCK_UN)
textfile.close()
textfile = open(textpath, mode='r')
flock(textfile, LOCK_SH)
except (ValueError, IOError) as e:
break
try:
textfile.seek(offset, 0)
except ValueError:
# file was closed, settle with what we have and continue
break
textdata += textfile.read(length)
fcntl.flock(textfile, fcntl.LOCK_UN)
textfile.close()
try:
flock(textfile, LOCK_UN)
textfile.close()
except:
pass
if termstate is None:
termstate = 0
return textdata, termstate, recenttimestamp
@@ -270,8 +731,21 @@ 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
globaleventlog = None
tracelog = None
def log(logdata=None, ltype=None, event=0, eventdata=None):
if globaleventlog is None:
globaleventlog = Logger('events')
globaleventlog.log(logdata, ltype, event, eventdata)
def logtrace():
global tracelog
if tracelog is None:
tracelog = Logger('trace')
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
+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
from eventlet.support import greendns
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 greendns.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 greendns.getaddrinfo(currhm, None):
curraddr = curraddr[4][0]
if curraddr in manageraddresses:
manager_to_nodemap[manager] = node
return node
+118 -19
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -27,21 +27,42 @@
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
dbgif = False
if map(int, (eventlet.__version__.split('.'))) > [0, 18]:
import eventlet.backdoor as backdoor
dbgif = True
havefcntl = True
try:
import fcntl
except ImportError:
havefcntl = False
#import multiprocessing
import gc
from greenlet import greenlet
import sys
import os
import signal
import socket
import time
import traceback
def _daemonize():
if not 'fork' in os.__dict__:
return
thispid = os.fork()
if thispid > 0:
os.waitpid(thispid, 0)
@@ -74,9 +95,15 @@ def _checkpidfile():
fcntl.flock(pidfile, fcntl.LOCK_EX)
pid = pidfile.read()
if pid != '':
print ('/var/run/confluent/pid exists and indicates %s is still '
'running' % pid)
sys.exit(1)
try:
os.kill(int(pid), 0)
print ('/var/run/confluent/pid exists and indicates %s is still '
'running' % pid)
sys.exit(1)
except OSError:
# There is no process running by that pid, must be stale
pass
pidfile.seek(0)
pidfile.write(str(os.getpid()))
fcntl.flock(pidfile, fcntl.LOCK_UN)
pidfile.close()
@@ -106,32 +133,104 @@ 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)))
for o in gc.get_objects():
if not isinstance(o, greenlet):
continue
if not o:
continue
ht.write('Thread trace:\n')
ht.write(''.join(traceback.format_stack(o.gr_frame)))
ht.close()
def doexit():
if not havefcntl:
return
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass
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)
# We don't want to os._exit() until sync finishes from
# init above
configmanager.ConfigManager.wait_for_sync()
def setlimits():
try:
import resource
currlimit = resource.getrlimit(resource.RLIMIT_NOFILE)
if currlimit[0] < currlimit[1]:
resource.setrlimit(
resource.RLIMIT_NOFILE, (currlimit[1], currlimit[1]))
except Exception:
pass
def run():
_checkpidfile()
confluentcore.load_plugins()
setlimits()
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)
#TODO(jbjohnso): eventlet has a bug about unix domain sockets, this code
#works with bugs fixed
#dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
# family=socket.AF_UNIX)
#eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
consoleserver.start_console_sessions()
webservice = httpapi.HttpApi()
if dbgif:
oumask = os.umask(0077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass # We are not expecting the file to exist
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
os.umask(oumask)
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
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)
eventlet.sleep(1)
consoleserver.start_console_sessions()
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)
+656 -59
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -19,8 +19,17 @@
# Things are defined here to 'encourage' developers to coordinate information
# format. This is also how different data formats are supported
import confluent.exceptions as exc
from copy import deepcopy
from datetime import datetime
import json
valid_health_values = set([
'ok',
'warning',
'critical',
'failed',
'unknown',
])
def _htmlify_structure(indict):
ret = "<ul>"
@@ -29,12 +38,20 @@ def _htmlify_structure(indict):
ret += "<li>{0}: ".format(key)
if type(indict[key]) in (str, unicode, float, int):
ret += str(indict[key])
elif isinstance(indict[key], datetime):
ret += indict[key].strftime('%Y-%m-%dT%H:%M:%S')
else:
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)
@@ -42,6 +59,7 @@ def _htmlify_structure(indict):
class ConfluentMessage(object):
apicode = 200
readonly = False
defaultvalue = ''
defaulttype = 'text'
@@ -65,7 +83,8 @@ class ConfluentMessage(object):
"""Return pythonic representation of the response.
Used by httpapi while assembling data prior to json serialization"""
if hasattr(self, 'stripped') and self.stripped:
if ((hasattr(self, 'stripped') and self.stripped) or
(hasattr(self, 'notnode') and self.notnode)):
return self.kvpairs
return {'databynode': self.kvpairs}
@@ -75,7 +94,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 +113,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 +161,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) + ')'
@@ -152,32 +175,40 @@ class ConfluentMessage(object):
class ConfluentNodeError(object):
apicode = 500
def __init__(self, node, errorstr):
self.node = node
self.error = errorstr
def raw(self):
return {'databynode': {self.node: {'error': self.error}}}
return {'databynode': {self.node: {'errorcode': self.apicode,
'error': self.error}}}
def html(self):
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):
apicode = 504
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):
apicode = 404
def __init__(self, node, errorstr='not found'):
self.node = node
self.error = errorstr
@@ -185,7 +216,9 @@ class ConfluentTargetNotFound(ConfluentNodeError):
def strip_node(self, node):
raise exc.NotFoundException(self.error)
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
apicode = 502
def __init__(self, node):
self.node = node
self.error = 'bad credentials'
@@ -248,7 +281,6 @@ class LinkRelation(ConfluentMessage):
self.href = ''
self.rel = ''
def json(self):
"""Provide json_hal style representation of the relation.
@@ -293,19 +325,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 = {}
@@ -348,14 +429,14 @@ class InputAttributes(ConfluentMessage):
def get_attributes(self, node):
if node not in self.nodeattribs:
return {}
nodeattr = self.nodeattribs[node]
nodeattr = deepcopy(self.nodeattribs[node])
for attr in nodeattr:
if type(nodeattr[attr]) in (str, unicode):
try:
# 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 +444,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 = deepcopy(self.credentials[node])
for attr in credential:
if type(credential[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 +527,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 +543,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 +568,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',
@@ -435,10 +704,10 @@ class BootDevice(ConfluentChoiceMessage):
valid_paramset = {
'bootmode': valid_bootmodes,
'persistent': set([True, False]),
}
def __init__(self, node, device, bootmode='unspecified'):
def __init__(self, node, device, bootmode='unspecified', persistent=False):
if device not in self.valid_values:
raise Exception("Invalid boot device argument passed in:" +
repr(device))
@@ -448,7 +717,8 @@ class BootDevice(ConfluentChoiceMessage):
self.kvpairs = {
node: {
'nextdevice': {'value': device},
'bootmode': {'value': bootmode },
'bootmode': {'value': bootmode},
'persistent': {'value': persistent},
}
}
@@ -457,6 +727,7 @@ class InputBootDevice(BootDevice):
def __init__(self, path, nodes, inputdata):
self.bootdevbynode = {}
self.bootmodebynode = {}
self.persistentbynode = {}
if not inputdata:
raise exc.InvalidArgumentException()
if 'nextdevice' not in inputdata:
@@ -478,6 +749,8 @@ class InputBootDevice(BootDevice):
datum['bootmode'] + ' is not one of ' +
','.join(self.valid_bootmodes))
self.bootmodebynode[key] = datum['bootmode']
if 'persistent' in datum:
self.bootmodebynode[key] = datum['persistent']
else:
datum = inputdata
if 'nextdevice' not in datum:
@@ -491,6 +764,8 @@ class InputBootDevice(BootDevice):
self.bootdevbynode[node] = datum['nextdevice']
if 'bootmode' in datum:
self.bootmodebynode[node] = datum['bootmode']
if 'persistent' in datum:
self.persistentbynode[node] = datum['persistent']
def bootdevice(self, node):
return self.bootdevbynode[node]
@@ -498,6 +773,9 @@ class InputBootDevice(BootDevice):
def bootmode(self, node):
return self.bootmodebynode.get(node, 'unspecified')
def persistent(self, node):
return self.persistentbynode.get(node, False)
class IdentifyState(ConfluentChoiceMessage):
valid_values = set([
@@ -514,10 +792,205 @@ 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 AsyncCompletion(ConfluentMessage):
def __init__(self):
self.stripped = True
self.notnode = True
def raw(self):
return {'_requestdone': True}
class AsyncMessage(ConfluentMessage):
def __init__(self, pair):
self.stripped = True
self.notnode = True
self.msgpair = pair
def raw(self):
rsp = self.msgpair[1]
rspdict = None
if (isinstance(rsp, ConfluentMessage) or
isinstance(rsp, ConfluentNodeError)):
rspdict = rsp.raw()
elif isinstance(rsp, exc.ConfluentException):
rspdict = {'exceptioncode': rsp.apierrorcode,
'exception': rsp.get_error_body()}
elif isinstance(rsp, Exception):
rspdict = {'exceptioncode': 500, 'exception': str(rsp)}
elif isinstance(rsp, dict): # console metadata
rspdict = rsp
else: # terminal text
rspdict = {'data': rsp}
return {'asyncresponse':
{'requestid': self.msgpair[0],
'response': rspdict}}
class AsyncSession(ConfluentMessage):
def __init__(self, id):
self.desc = 'foo'
self.notnode = True
self.stripped = True
self.kvpairs = {'asyncid': id}
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 +1000,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 +1020,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 +1116,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 +1208,5 @@ class CryptedAttributes(Attributes):
self.kvpairs = nkv
else:
self.kvpairs = {
node: nkv
name: nkv
}
@@ -0,0 +1,249 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 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 provides the implementation of locating MAC addresses on ethernet
# switches. It is, essentially, a port of 'MacMap.pm' to confluent.
# However, there are enhancements.
# For one, each switch interrogation is handled in an eventlet 'thread'
# For another, MAC addresses are checked in the dictionary on every
# switch return, rather than waiting for all switches to check in
# (which makes it more responsive when there is a missing or bad switch)
# Also, we track the quantity, actual ifName value, and provide a mechanism
# to detect ambiguous result (e.g. if two matches are found, can log an error
# rather than doing the wrong one, complete with the detected ifName value).
# Further, the map shall be available to all facets of the codebase, not just
# the discovery process, so that the cached data maintenance will pay off
# for direct queries
# this module will provide mac to switch and full 'ifName' label
# This functionality is restricted to the null tenant
import confluent.exceptions as exc
import confluent.log as log
import confluent.snmputil as snmp
from eventlet.greenpool import GreenPool
import re
_macmap = {}
_macsbyswitch = {}
_nodesbymac = {}
_switchportmap = {}
_whitelistnames = (
# 3com
re.compile(r'^RMON Port (\d+) on unit \d+'),
# Dell
re.compile(r'^Unit \d+ Port (\d+)\Z'),
)
_blacklistnames = (
re.compile(r'vl'),
re.compile(r'Nu'),
re.compile(r'RMON'),
re.compile(r'onsole'),
re.compile(r'Stack'),
re.compile(r'Trunk'),
re.compile(r'po\d'),
re.compile(r'XGE'),
re.compile(r'LAG'),
re.compile(r'CPU'),
re.compile(r'Management'),
)
def _namesmatch(switchdesc, userdesc):
if switchdesc == userdesc:
return True
try:
portnum = int(userdesc)
except ValueError:
portnum = None
if portnum is not None:
for exp in _whitelistnames:
match = exp.match(switchdesc)
if match:
snum = int(match.groups()[0])
if snum == portnum:
return True
anymatch = re.search(r'[^0123456789]' + userdesc + r'(\.0)?\Z', switchdesc)
if anymatch:
for blexp in _blacklistnames:
if blexp.match(switchdesc):
return False
return True
return False
def _map_switch(args):
try:
return _map_switch_backend(args)
except Exception as e:
log.logtrace()
def _nodelookup(switch, ifname):
"""Get a nodename for a given switch and interface name
"""
for portdesc in _switchportmap.get(switch, {}):
if _namesmatch(ifname, portdesc):
return _switchportmap[switch][portdesc]
return None
def _map_switch_backend(args):
"""Manipulate portions of mac address map relevant to a given switch
"""
# 1.3.6.1.2.1.17.7.1.2.2.1.2 - mactoindex (qbridge - preferred)
# if not, check for cisco and if cisco, build list of all relevant vlans:
# .1.3.6.1.4.1.9.9.46.1.6.1.1.5 - trunk port vlan map (cisco only)
# .1.3.6.1.4.1.9.9.68.1.2.2.1.2 - access port vlan map (cisco only)
# if cisco, vlan community string indexed or snmpv3 contest for:
# 1.3.6.1.2.1.17.4.3.1.2 - mactoindx (bridge - low-end switches and cisco)
# .1.3.6.1.2.1.17.1.4.1.2 - bridge index to if index map
# no vlan index or context for:
# .1.3.6.1.2.1.31.1.1.1.1 - ifName... but some switches don't do it
# .1.3.6.1.2.1.2.2.1.2 - ifDescr, usually useless, but a
# fallback if ifName is empty
#
global _macmap
switch, password, user = args
haveqbridge = False
mactobridge = {}
conn = snmp.Session(switch, password, user)
for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
haveqbridge = True
oid, bridgeport = vb
if not bridgeport:
continue
oid = str(oid).rsplit('.', 6) # if 7, then oid[1] would be vlan id
macaddr = '{0:02x}:{1:02x}:{2:02x}:{3:02x}:{4:02x}:{5:02x}'.format(
*([int(x) for x in oid[-6:]])
)
mactobridge[macaddr] = int(bridgeport)
if not haveqbridge:
raise exc.NotImplementedException('TODO: Bridge-MIB without QBRIDGE')
bridgetoifmap = {}
for vb in conn.walk('1.3.6.1.2.1.17.1.4.1.2'):
bridgeport, ifidx = vb
bridgeport = int(str(bridgeport).rsplit('.', 1)[1])
bridgetoifmap[bridgeport] = int(ifidx)
ifnamemap = {}
havenames = False
for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
ifidx, ifname = vb
if not ifname:
continue
havenames = True
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
if not havenames:
for vb in conn.walk( '1.3.6.1.2.1.2.2.1.2'):
ifidx, ifname = vb
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
maccounts = {}
for mac in mactobridge:
ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]]
if ifname not in maccounts:
maccounts[ifname] = 1
else:
maccounts[ifname] += 1
_macsbyswitch[switch] = {}
for mac in mactobridge:
# We want to merge it so that when a mac appears in multiple
# places, it is captured.
ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]]
if mac in _macmap:
_macmap[mac].append((switch, ifname, maccounts[ifname]))
else:
_macmap[mac] = [(switch, ifname, maccounts[ifname])]
if ifname in _macsbyswitch[switch]:
_macsbyswitch[switch][ifname].append(mac)
else:
_macsbyswitch[switch][ifname] = [mac]
nodename = _nodelookup(switch, ifname)
if nodename is not None:
if mac in _nodesbymac and _nodesbymac[mac] != nodename:
log.log({'warning': '{0} and {1} described by ambiguous'
' switch topology values'.format(nodename,
_nodesbymac[mac]
)})
_nodesbymac[mac] = nodename
def update_macmap(configmanager):
"""Interrogate switches to build/update mac table
Begin a rebuild process. This process is a generator that will yield
as each switch interrogation completes, allowing a caller to
recheck the cache as results become possible, rather
than having to wait for the process to complete to interrogate.
"""
global _macmap
global _nodesbymac
global _switchportmap
# Clear all existing entries
_macmap = {}
_nodesbymac = {}
_switchportmap = {}
if configmanager.tenant is not None:
raise exc.ForbiddenRequest('Network topology not available to tenants')
nodelocations = configmanager.get_node_attributes(
configmanager.list_nodes(), ('hardwaremanagement.switch',
'hardwaremanagement.switchport'))
switches = set([])
for node in nodelocations:
cfg = nodelocations[node]
if 'hardwaremanagement.switch' in cfg:
curswitch = cfg['hardwaremanagement.switch']['value']
switches.add(curswitch)
if 'hardwaremanagement.switchport' in cfg:
portname = cfg['hardwaremanagement.switchport']['value']
if curswitch not in _switchportmap:
_switchportmap[curswitch] = {}
if portname in _switchportmap[curswitch]:
log.log({'warning': 'Duplicate switch topology config for '
'{0} and {1}'.format(node,
_switchportmap[
curswitch][
portname])})
_switchportmap[curswitch][portname] = node
switchcfg = configmanager.get_node_attributes(
switches, ('secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'), decrypt=True)
switchauth = []
for switch in switches:
password = 'public'
user = None
if (switch in switchcfg and
'secret.hardwaremanagementpassword' in switchcfg[switch]):
password = switchcfg[switch]['secret.hardwaremanagementpassword'][
'value']
if 'secret.hardwaremanagementuser' in switchcfg[switch]:
user = switchcfg[switch]['secret.hardwaremanagementuser'][
'value']
switchauth.append((switch, password, user))
pool = GreenPool()
for res in pool.imap(_map_switch, switchauth):
yield res
print(repr(_macmap))
if __name__ == '__main__':
# invoke as switch community
import sys
_map_switch(sys.argv[1], sys.argv[2])
+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
@@ -152,6 +152,20 @@ def update_nodegroup(group, element, configmanager, inputdata):
return retrieve_nodegroup(group, element, configmanager, inputdata)
def _expand_expression(nodes, configmanager, inputdata):
expression = inputdata.get_attributes(list(nodes)[0])
if type(expression) is dict:
expression = expression['expression']
if type(expression) is dict:
expression = expression['expression']
for expanded in configmanager.expand_attrib_expression(nodes, expression):
yield msg.KeyValueData({'value': expanded[1]}, expanded[0])
def create(nodes, element, configmanager, inputdata):
if nodes is not None and element[-1] == 'expression':
return _expand_expression(nodes, configmanager, inputdata)
def update_nodes(nodes, element, configmanager, inputdata):
updatedict = {}
for node in nodes:
@@ -1,5 +1,5 @@
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -13,22 +13,32 @@
# 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
import confluent.util as util
import eventlet
import eventlet.event
import eventlet.green.threading as threading
import eventlet.greenpool as greenpool
import eventlet.queue as queue
import eventlet.support.greendns
import pyghmi.constants as pygconstants
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.console as console
import pyghmi.ipmi.command as ipmicommand
console = eventlet.import_patched('pyghmi.ipmi.console')
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
import socket
console.session.select = eventlet.green.select
console.session.threading = eventlet.green.threading
console.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
def exithandler():
console.session.iothread.join()
atexit.register(exithandler)
_ipmiworkers = greenpool.GreenPool()
@@ -42,18 +52,67 @@ sensor_categories = {
}
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):
def __init__(self, node, cfm, **kwargs):
self.cfm = cfm
self.node = node
self._inhealth = False
self._lasthealth = None
self._attribwatcher = cfm.watch_attributes(
(node,), ('secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword', 'secret.ipmikg',
'hardwaremanagement.manager'), self._attribschanged)
super(self.__class__, self).__init__(**kwargs)
def setup_confluent_keyhandler(self):
self.register_key_handler(util.TLSCertVerifier(
self.cfm, self.node, 'pubkeys.tls_hardwaremanager').verify_cert)
def close_confluent(self):
if self._attribwatcher:
self.cfm.remove_watcher(self._attribwatcher)
self._attribwatcher = None
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
try:
self.ipmi_session._mark_broken()
@@ -62,6 +121,20 @@ class IpmiCommandWrapper(ipmicommand.Command):
# then do nothing
pass
def get_health(self):
if self._inhealth:
while self._inhealth:
eventlet.sleep(0.1)
return self._lasthealth
self._inhealth = True
try:
self._lasthealth = super(IpmiCommandWrapper, self).get_health()
except Exception:
self._inhealth = False
raise
self._inhealth = False
return self._lasthealth
def _ipmi_evtloop():
while True:
@@ -146,6 +219,7 @@ class IpmiConsole(conapi.Console):
self.broken = True
self.error = data['error']
if self.connected:
self.connected = False
self.datacallback(conapi.ConsoleEvent.Disconnect)
else:
self.datacallback(data)
@@ -223,22 +297,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
@@ -257,6 +331,10 @@ class IpmiHandler(object):
if ((node, tenant) not in persistent_ipmicmds or
not persistent_ipmicmds[(node, tenant)].ipmi_session.logged):
self._logevt = threading.Event()
try:
persistent_ipmicmds[(node, tenant)].close_confluent()
except KeyError: # was no previous session
pass
try:
persistent_ipmicmds[(node, tenant)] = IpmiCommandWrapper(
node, cfg, bmc=connparams['bmc'],
@@ -266,6 +344,7 @@ class IpmiHandler(object):
except socket.gaierror as ge:
if ge[0] == -2:
raise exc.TargetEndpointUnreachable(ge[1])
raise
self.ipmicmd = persistent_ipmicmds[(node, tenant)]
bootdevices = {
@@ -277,7 +356,9 @@ class IpmiHandler(object):
self.broken = True
self.error = response['error']
else:
self.ipmicmd = ipmicmd
self.loggedin = True
self.ipmicmd.setup_confluent_keyhandler()
self._logevt.set()
def handle_request(self):
@@ -285,12 +366,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 +389,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 +594,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 +607,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 +736,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')
@@ -398,15 +754,22 @@ class IpmiHandler(object):
bootmode = 'uefi'
else:
bootmode = 'bios'
persistent = False
if 'persistent' in bootdev:
persistent = bootdev['persistent']
self.output.put(msg.BootDevice(node=self.node,
device=bootdev['bootdev'],
bootmode=bootmode))
bootmode=bootmode,
persistent=persistent))
return
elif 'update' == self.op:
bootdev = self.inputdata.bootdevice(self.node)
douefi = False
if self.inputdata.bootmode(self.node) == 'uefi':
douefi = True
bootdev = self.ipmicmd.set_bootdev(bootdev, uefiboot=douefi)
persistent = self.inputdata.persistent(self.node)
bootdev = self.ipmicmd.set_bootdev(bootdev, uefiboot=douefi,
persist=persistent)
if bootdev['bootdev'] in self.bootdevices:
bootdev['bootdev'] = self.bootdevices[bootdev['bootdev']]
self.output.put(msg.BootDevice(node=self.node,
@@ -418,24 +781,108 @@ 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()
if len(servers) > idx:
self.output.put(msg.NTPServer(self.node, servers[idx]))
else:
self.output.put(
msg.ConfluentTargetNotFound(
self.node, 'Requested NTP configuration not found'))
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(servers[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 isinstance(health, str):
return health
if pygconstants.Health.Failed & health:
health = 'failed'
elif pygconstants.Health.Critical & health:
@@ -471,4 +918,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)
@@ -0,0 +1,161 @@
# 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 plugin provides an ssh implementation comforming to the 'console'
# specification. consoleserver or shellserver would be equally likely
# to use this.
import confluent.exceptions as cexc
import confluent.interface.console as conapi
import confluent.log as log
import eventlet
import hashlib
paramiko = eventlet.import_patched('paramiko')
class HostKeyHandler(paramiko.client.MissingHostKeyPolicy):
def __init__(self, configmanager, node):
self.cfm = configmanager
self.node = node
def missing_host_key(self, client, hostname, key):
fingerprint = 'sha512$' + hashlib.sha512(key.asbytes()).hexdigest()
cfg = self.cfm.get_node_attributes(
self.node, ('pubkeys.ssh', 'pubkeys.addpolicy'))
if 'pubkeys.ssh' not in cfg[self.node]:
if ('pubkeys.addpolicy' in cfg[self.node] and
cfg[self.node]['pubkeys.addpolicy'] and
cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
raise cexc.PubkeyInvalid('New ssh key detected',
key.asbytes(), fingerprint,
'pubkeys.ssh', 'newkey')
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'sshautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {'pubkeys.ssh': fingerprint}})
return True
elif cfg[self.node]['pubkeys.ssh']['value'] == fingerprint:
return True
raise cexc.PubkeyInvalid(
'Mismatched SSH host key detected', key.asbytes(), fingerprint,
'pubkeys.ssh', 'mismatch'
)
class SshShell(conapi.Console):
def __init__(self, node, config, username='', password=''):
self.node = node
self.ssh = None
self.nodeconfig = config
self.username = username
self.password = password
self.inputmode = 0 # 0 = username, 1 = password...
def recvdata(self):
while self.connected:
pendingdata = self.shell.recv(8192)
if pendingdata == '':
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
self.datacallback(pendingdata)
def connect(self, callback):
# for now, we just use the nodename as the presumptive ssh destination
# TODO(jjohnson2): use a 'nodeipget' utility function for architectures
# that would rather not use the nodename as anything but an opaque
# identifier
self.datacallback = callback
if self.username is not '':
self.logon()
else:
self.inputmode = 0
callback('\r\nlogin as: ')
return
def logon(self):
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(
HostKeyHandler(self.nodeconfig, self.node))
try:
self.ssh.connect(self.node, username=self.username,
password=self.password, allow_agent=False,
look_for_keys=False)
except paramiko.AuthenticationException:
self.inputmode = 0
self.username = ''
self.password = ''
self.datacallback('\r\nlogin as: ')
return
except paramiko.ssh_exception.NoValidConnectionsError as e:
self.datacallback(str(e))
self.inputmode = 0
self.username = ''
self.password = ''
self.datacallback('\r\nlogin as: ')
return
self.inputmode = 2
self.connected = True
self.shell = self.ssh.invoke_shell()
self.rxthread = eventlet.spawn(self.recvdata)
def write(self, data):
if self.inputmode == 0:
while len(data) and data[0] == b'\x7f' and len(self.username):
self.datacallback('\b \b') # erase previously echoed value
self.username = self.username[:-1]
data = data[1:]
while len(data) and data[0] == b'\x7f':
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.username += data
if '\r' in self.username:
self.username, self.password = self.username.split('\r')
lastdata = data.split('\r')[0]
if lastdata != '':
self.datacallback(lastdata)
self.datacallback('\r\nEnter password: ')
self.inputmode = 1
elif len(data) > 0:
# echo back typed data
self.datacallback(data)
elif self.inputmode == 1:
while len(data) > 0 and data[0] == b'\x7f':
self.password = self.password[:-1]
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.password += data
if '\r' in self.password:
self.password = self.password.split('\r')[0]
self.datacallback('\r\n')
self.logon()
else:
self.shell.sendall(data)
def close(self):
if self.ssh is not None:
self.ssh.close()
def create(nodes, element, configmanager, inputdata):
if len(nodes) == 1:
return SshShell(nodes[0], configmanager)
+126
View File
@@ -0,0 +1,126 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 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 module tracks each node, tenants currently active shell sessions
# 'ConsoleSession' objects from consoleserver are used, but with the additional
# capacity for having a multiple of sessions per node active at a given time
import confluent.consoleserver as consoleserver
import confluent.exceptions as exc
import confluent.messages as msg
activesessions = {}
class _ShellHandler(consoleserver.ConsoleHandler):
_plugin_path = '/nodes/{0}/_shell/session'
_genwatchattribs = False
_logtobuffer = False
def log(self, *args, **kwargs):
# suppress logging through proving a stub 'log' function
return
def _got_disconnected(self):
self.connectstate = 'closed'
self._send_rcpts({'connectstate': self.connectstate})
for session in list(self.livesessions):
session.destroy()
def get_sessions(tenant, node, user):
"""Get sessionids active for node
Given a tenant, nodename, and user; provide an iterable of sessionids.
Each permutation of tenant, nodename and user have a distinct set of shell
sessions.
:param tenant: The tenant identifier for the current scope
:param node: The nodename of the current scope.
:param user: The confluent user that will 'own' the session.
"""
return activesessions.get((tenant, node, user), {})
def get_session(tenant, node, user, sessionid):
return activesessions.get((tenant, node, user), {}).get(sessionid, None)
class ShellSession(consoleserver.ConsoleSession):
"""Create a new socket to converse with a node shell session
This object provides a filehandle that can be read/written
too in a normal fashion and the concurrency, logging, and
event watching will all be handled seamlessly. It represents a remote
CLI shell session.
:param node: Name of the node for which this session will be created
:param configmanager: A configuration manager object for current context
:param username: Username for which this session object will operate
:param datacallback: An asynchronous data handler, to be called when data
is available. Note that if passed, it makes
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
:param sessionid: An optional identifier to match a running session or
customize the name of a new session.
"""
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False, sessionid=None):
self.sessionid = sessionid
self.configmanager = configmanager
self.node = node
super(ShellSession, self).__init__(node, configmanager, username,
datacallback, skipreplay)
def connect_session(self):
global activesessions
tenant = self.configmanager.tenant
if (self.configmanager.tenant, self.node) not in activesessions:
activesessions[(tenant, self.node, self.username)] = {}
if self.sessionid is None:
self.sessionid = 1
while str(self.sessionid) in activesessions[(tenant, self.node, self.username)]:
self.sessionid += 1
self.sessionid = str(self.sessionid)
if self.sessionid not in activesessions[(tenant, self.node, self.username)]:
activesessions[(tenant, self.node, self.username)][self.sessionid] = _ShellHandler(self.node, self.configmanager)
self.conshdl = activesessions[(self.configmanager.tenant, self.node, self.username)][self.sessionid]
def destroy(self):
try:
del activesessions[(self.configmanager.tenant, self.node,
self.username)][self.sessionid]
except KeyError:
pass
super(ShellSession, self).destroy()
def create(nodes, element, configmanager, inputdata):
# For creating a resource, it really has to be handled
# in httpapi/sockapi specially, like a console.
raise exc.InvalidArgumentException('Special client code required')
def retrieve(nodes, element, configmanager, inputdata):
tenant = configmanager.tenant
user = configmanager.current_user
if (tenant, nodes[0], user) in activesessions:
for sessionid in activesessions[(tenant, nodes[0], user)]:
yield msg.ChildCollection(sessionid)
+103
View File
@@ -0,0 +1,103 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 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 provides a simplified wrapper around snmp implementation roughly
# mapping to the net-snmp commands
# net-snmp-python was considered as the API is cleaner, but the ability to
# patch pysnmp to have it be eventlet friendly has caused it's selection
# This module simplifies the complex hlapi pysnmp interface
import confluent.exceptions as exc
import eventlet
from eventlet.support.greendns import getaddrinfo
import socket
snmp = eventlet.import_patched('pysnmp.hlapi')
def _get_transport(name):
# Annoyingly, pysnmp does not automatically determine ipv6 v ipv4
res = getaddrinfo(name, 161, 0, socket.SOCK_DGRAM)
if res[0][0] == socket.AF_INET6:
return snmp.Udp6TransportTarget(res[0][4])
else:
return snmp.UdpTransportTarget(res[0][4])
class Session(object):
def __init__(self, server, secret, username=None, context=None):
"""Create a new session to interrogate a switch
If username is not given, it is assumed that
the secret is community string, and v2c is used. If a username given,
it'll assume SHA auth and DES privacy with the secret being the same
for both.
:param server: The network name/address to target
:param secret: The community string or password
:param username: The username for SNMPv3
:param context: The SNMPv3 context or index for community indexing
"""
self.server = server
self.context = context
if username is None:
# SNMP v2c
self.authdata = snmp.CommunityData(secret, mpModel=1)
else:
self.authdata = snmp.UsmUserData(username, authKey=secret,
privKey=secret)
self.eng = snmp.SnmpEngine()
def walk(self, oid):
"""Walk over children of a given OID
This is roughly equivalent to snmpwalk. It will automatically try to
be a snmpbulkwalk if possible.
:param oid: The SNMP object identifier
"""
# SNMP is a complicated mess of things. Will endeavor to shield caller
# from as much as possible, assuming reasonable defaults when possible.
# there may come a time where we add more parameters to override the
# automatic behavior (e.g. DES is weak, so it's likely to be
# overriden, but some devices only support DES)
tp = _get_transport(self.server)
ctx = snmp.ContextData(self.context)
if '::' in oid:
mib, field = oid.split('::')
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
else:
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
lexicographicMode=False)
for rsp in walking:
errstr, errnum, erridx, answers = rsp
if errstr:
raise exc.TargetEndpointUnreachable(str(errstr))
elif errnum:
raise exc.ConfluentException(errnum.prettyPrint())
for ans in answers:
yield ans
if __name__ == '__main__':
import sys
ts = Session(sys.argv[1], 'public')
for kp in ts.walk(sys.argv[2]):
print(str(kp[0]))
print(str(kp[1]))
+112 -84
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,9 +18,10 @@
# This is the socket api layer.
# It implement unix and tls sockets
#
#
import atexit
import errno
import os
import pwd
import stat
@@ -38,6 +40,7 @@ import confluent.config.configmanager as configmanager
import confluent.exceptions as exc
import confluent.log as log
import confluent.core as pluginapi
import confluent.shellserver as shellserver
tracelog = None
@@ -62,15 +65,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
@@ -78,15 +89,15 @@ def sessionhdl(connection, authname, skipauth=False):
cfm = None
if skipauth:
authenticated = True
cfm = configmanager.ConfigManager(tenant=None)
cfm = configmanager.ConfigManager(tenant=None, username=authname)
elif authname:
authdata = auth.authorize(authname, element=None)
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 +112,29 @@ 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,
'error': 'Forbidden'})
tlvdata.send(connection, {'_requestdone': 1})
except exc.TargetEndpointBadCredentials:
tlvdata.send(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})
except exc.NotImplementedException:
tlvdata.send(connection, {'errorcode': 501,
'error': 'Not Implemented'})
tlvdata.send(connection, {'_requestdone': 1})
except exc.NotFoundException as nfe:
tlvdata.send(connection, {'errorcode': 404,
'error': str(nfe)})
tlvdata.send(connection, {'_requestdone': 1})
except exc.InvalidArgumentException as iae:
tlvdata.send(connection, {'errorcode': 400,
'error': 'Bad Request - ' + str(iae)})
tlvdata.send(connection, {'_requestdone': 1})
except exc.ConfluentException as e:
if ((not isinstance(e, exc.LockedCredentials)) and
e.apierrorcode == 500):
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
send_data(connection, {'errorcode': e.apierrorcode,
'error': e.apierrorstr,
'detail': e.get_error_body()})
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,91 +142,118 @@ 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):
if not isinstance(request, dict):
raise ValueError
raise exc.InvalidArgumentException
operation = request['operation']
path = request['path']
params = request.get('parameters', None)
params = request.get('parameters', {})
hdlr = None
if not skipauth:
authdata = auth.authorize(authdata[2], path, authdata[3], operation)
auditmsg = {
'operation': operation,
'user': authdata[2],
'target': path,
}
if authdata[3] is not None:
auditmsg['tenant'] = authdata[3]
if authdata is None:
auditmsg['allowed'] = False
auditlog.log(auditmsg)
raise exc.ForbiddenRequest()
auditmsg['user'] = authdata[2]
if authdata[3] is not None:
auditmsg['tenant'] = authdata[3]
auditmsg['allowed'] = True
auditlog.log(auditmsg)
try:
if operation == 'start':
elems = path.split('/')
if elems[3] != "console":
raise exc.InvalidArgumentException()
node = elems[2]
ccons = ClientConsole(connection)
skipreplay = False
if params and 'skipreplay' in params and params['skipreplay']:
skipreplay = True
consession = consoleserver.ConsoleSession(
node=node, configmanager=cfm, username=authname,
datacallback=ccons.sendall, skipreplay=skipreplay)
if consession is None:
raise Exception("TODO")
tlvdata.send(connection, {'started': 1})
ccons.startsending()
bufferage = consession.get_buffer_age()
if bufferage is not False:
tlvdata.send(connection, {'bufferage': bufferage})
while consession is not None:
data = tlvdata.recv(connection)
if type(data) == dict:
if data['operation'] == 'stop':
consession.destroy()
return
elif data['operation'] == 'break':
consession.send_break()
continue
elif data['operation'] == 'reopen':
consession.reopen()
continue
else:
raise Exception("TODO")
if not data:
consession.destroy()
return
consession.write(data)
return start_term(authname, cfm, connection, params, path)
elif operation == 'shutdown':
configmanager.ConfigManager.shutdown()
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 start_term(authname, cfm, connection, params, path):
elems = path.split('/')
if len(elems) < 4 or elems[1] != 'nodes':
raise exc.InvalidArgumentException('Invalid path {0}'.format(path))
node = elems[2]
ccons = ClientConsole(connection)
skipreplay = False
if params and 'skipreplay' in params and params['skipreplay']:
skipreplay = True
if elems[3] == "console":
consession = consoleserver.ConsoleSession(
node=node, configmanager=cfm, username=authname,
datacallback=ccons.sendall, skipreplay=skipreplay)
elif len(elems) >= 6 and elems[3:5] == ['shell', 'sessions']:
if len(elems) == 7:
sessionid = elems[5]
else:
sessionid = None
consession = shellserver.ShellSession(
node=node, configmanager=cfm, username=authname,
datacallback=ccons.sendall, skipreplay=skipreplay,
sessionid=sessionid)
else:
raise exc.InvalidArgumentException('Invalid path {0}'.format(path))
if consession is None:
raise Exception("TODO")
send_data(connection, {'started': 1})
ccons.startsending()
bufferage = consession.get_buffer_age()
if bufferage is not False:
send_data(connection, {'bufferage': bufferage})
while consession is not None:
data = tlvdata.recv(connection)
if type(data) == dict:
if data['operation'] == 'stop':
consession.destroy()
return
elif data['operation'] == 'break':
consession.send_break()
continue
elif data['operation'] == 'reopen':
consession.reopen()
continue
else:
raise Exception("TODO")
if not data:
consession.destroy()
return
consession.write(data)
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))
bound = False
while not bound:
try:
plainsocket.bind((bind_host, bind_port, 0, 0))
bound = True
except socket.error as e:
if e.errno != 98:
raise
sys.stderr.write('TLS Socket in use, retrying in 1 second\n')
eventlet.sleep(1)
plainsocket.listen(5)
while (1): # TODO: exithook
cnn, addr = plainsocket.accept()
@@ -257,6 +280,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 +314,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)
+40
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,6 +17,9 @@
# Various utility functions that do not neatly fit into one category or another
import base64
import confluent.exceptions as cexc
import confluent.log as log
import hashlib
import os
import struct
@@ -56,3 +60,39 @@ def monotonic_time():
"""
# for now, just support POSIX systems
return os.times()[4]
class TLSCertVerifier(object):
def __init__(self, configmanager, node, fieldname):
self.cfm = configmanager
self.node = node
self.fieldname = fieldname
def verify_cert(self, certificate):
fingerprint = 'sha512$' + hashlib.sha512(certificate).hexdigest()
storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
)
if self.fieldname not in storedprint[self.node]: # no stored value, check
# policy for next action
newpolicy = self.cfm.get_node_attributes(self.node,
('pubkeys.addpolicy',))
if ('pubkeys.addpolicy' in newpolicy[self.node] and
'value' in newpolicy[self.node]['pubkeys.addpolicy'] and
newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
# manual policy means always raise unless a match is set
# manually
raise cexc.PubkeyInvalid('New certificate detected',
certificate, fingerprint,
self.fieldname, 'newkey')
# since the policy is not manual, go ahead and add new key
# after logging to audit log
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'certautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {self.fieldname: fingerprint}})
return True
elif storedprint[self.node][self.fieldname]['value'] == fingerprint:
return True
raise cexc.PubkeyInvalid(
'Mismatched certificate detected', certificate, fingerprint,
self.fieldname, 'mismatch')
+4 -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: python-pyghmi, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dns
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
Url: http://xcat.sf.net/
@@ -33,6 +33,9 @@ done
grep -v confluent/__init__.py INSTALLED_FILES.bare > INSTALLED_FILES
cat INSTALLED_FILES
%post
if [ -x /usr/bin/systemctl ]; then /usr/bin/systemctl try-restart confluent; fi
%clean
rm -rf $RPM_BUILD_ROOT
+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 describe|cut -d- -f 1`
NUMCOMMITS=`git describe|cut -d- -f 2`
if [ "$NUMCOMMITS" != "$VERSION" ]; then
VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3`
fi
echo $VERSION > VERSION
sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py
+3 -1
View File
@@ -10,11 +10,13 @@ setup(
description='confluent systems management server',
packages=['confluent', 'confluent/config', 'confluent/interface',
'confluent/plugins/hardwaremanagement/',
'confluent/plugins/shell/',
'confluent/plugins/configuration/'],
install_requires=['pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
install_requires=['paramiko', 'pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
'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