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

Compare commits

...

409 Commits

Author SHA1 Message Date
Jarrod Johnson 6973736c6a Set password before setting username
The BMC is at much higher risk for rejecting the password
(e.g. the password does not pass complexity requirements).  If the
username changes, but the password is still default, it is very
confusing.  Give the password change the chance to break the
configuration process first.
2018-10-23 09:16:15 -04:00
Jarrod Johnson f9055a258e Provide specific completion behavior for noderun/nodeshell 2018-10-23 09:09:03 -04:00
Jarrod Johnson c784a4ec9b Fix noderun output of continuing commands
noderun always only output at the end, fix the
mistake in the select statement.
2018-10-18 15:43:57 -04:00
Jarrod Johnson f2dd501de9 Do not proceed to try to upload if file doesn't exist
After relaying the error, it went ahead and
attempted the update, contrary to any reasonable expectation.
2018-10-15 11:15:13 -04:00
Jarrod Johnson e9ba49a4aa Intercept another 'unexpected error'
During a particularly hectic init, Invalid Session ID
may occur if a command is ran particularly early.  Intercept
and replace a more clean message.
2018-10-12 15:46:54 -04:00
Jarrod Johnson deb90fbca9 Fix trace on early console connect
If the trace happens before tracelog is ready, just print the output
to the stdout log for now.
2018-10-12 14:56:54 -04:00
Jarrod Johnson 3105b9b1f9 Significantly rework the collective startup behavior
One, make the tracking bools enforce a lock to reduce confusion

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

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

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

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

Avoid trying to assimilate existing followers.

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

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

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

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

Provide ability to check scan status through reading
/networking/macs/rescan
2018-08-28 11:10:32 -04:00
Jarrod Johnson 6aec9534e7 Fixes for nodesupport 2018-08-23 16:56:40 -04:00
Jarrod Johnson 3ee6334db2 Fix transfer owner argument for servicedata 2018-08-23 16:48:10 -04:00
Jarrod Johnson 582a4de62d Add CLI and directory support for nodesupport 2018-08-23 16:36:41 -04:00
Jarrod Johnson c9959d4082 More of the service data retrieval api
More progress is made toward the goal
2018-08-23 16:14:49 -04:00
Jarrod Johnson fa11fb54cb Add API support for getting service data
Service data retrieval is a common activity required
for interacting with support.
2018-08-23 15:39:25 -04:00
Jarrod Johnson ee3b824870 Add /description to nodes api 2018-08-23 08:44:41 -04:00
Jarrod Johnson 55f5b30369 Merge branch '21' 2018-08-22 16:29:58 -04:00
Jarrod Johnson 784e4bed2f Force cleanup if follow thread dies of exception
If something killed a follow thread, it was not always able to fire the
recovery off.  Wrap the risky code in a try statement.
2018-08-20 15:02:34 -04:00
Jarrod Johnson df7cba00fd Amend the message on collective failure 2018-08-17 16:45:45 -04:00
Jarrod Johnson dfb720d0ee Have collective command warn if the libssl library is not viable
Main example is RedHat providing pyOpenSSL of relatively ancient
vintage.
2018-08-17 13:57:13 -04:00
Jarrod Johnson f5d5cbd67b Have collective command warn if the libssl library is not viable
Main example is RedHat providing pyOpenSSL of relatively ancient
vintage.
2018-08-17 13:56:38 -04:00
Jarrod Johnson 319fec2145 Add advanced to nodeconfig 2018-08-17 11:16:11 -04:00
Jarrod Johnson 8787d23b3a Add advanced to API for system configuration.
pyghmi makes hiding optional advanced settings.
2018-08-17 10:59:50 -04:00
Jarrod Johnson 9b48110155 Do not proceed a logged, but broken session
It shouldn't be possible for this to be the case, but out of an
abundance of caution, check for this.  So far only produced this by
forcing broken = True in a debug session.  Intended to catch an alleged
scenario where console was managing to use a broken session (fixed in
pyghmi) and have confluent also recognize the situation for non-console
usage).
2018-08-16 14:43:16 -04:00
Jarrod Johnson 3064e7bef6 Ensure path is made prior to creating transactioncount
Fresh install will be missing /etc/confluent/cfg.  Advance the
_mkpath call to fix this problem.
2018-08-08 18:05:45 -04:00
Jarrod Johnson 1d4df8af3a Fix extraneous error in log on connectivity loss 2018-08-07 15:43:53 -04:00
Jarrod Johnson 2aba6e469c Correct variable name in the 'connected' fix 2018-08-07 15:31:41 -04:00
Jarrod Johnson de58593f14 Fix inability to notice underlying broken layers of the SOL
Through an unknown set of circumstances, an solconnection could be
stuck 'connecting'.  In every case analyzed, the ipmi_session was
broken.  Use that to detect a class of failure and react appropriately.
2018-08-07 15:12:53 -04:00
Jarrod Johnson ecbe1a86b1 Revert "Have nodeconsole restore term on exit"
This reverts commit 2972374da8.
2018-08-02 10:27:37 -04:00
Jarrod Johnson 2972374da8 Have nodeconsole restore term on exit 2018-08-02 10:07:41 -04:00
Jarrod Johnson 81dd6202d3 Fix when rpc has no 'exc' but has 'xid' 2018-07-30 11:26:09 -04:00
Jarrod Johnson 36a202842a Fix collective on rpc exception
Exceptions on collective calls were not correctly handled, fix
the handling so that collective continues and also the calling function
is correctly given the exception.
2018-07-30 09:33:24 -04:00
Jarrod Johnson 6a8e24dd0e Prioritize interactive feedback part of console handling. 2018-07-26 08:55:25 -04:00
Jarrod Johnson d3afeb3414 Fix web shell if user hits enter too fast 2018-07-24 17:20:22 -04:00
Jarrod Johnson 1bf4c0ac0a Have collective coalesce watched updates
Particularly chatty output can make collate be unreasonable in
low quality terminals and links.  Throttle to about 4 times a second.
2018-07-24 16:50:46 -04:00
Jarrod Johnson 8e422ef822 Fix ssh access
Fixed handler (e.g. ssh) did not return console consistent with
the plugin defined handlers.
2018-07-24 16:48:46 -04:00
Jarrod Johnson f0edbbad39 Have collective show present some info when not in quorum 2018-07-20 14:11:38 -04:00
Jarrod Johnson 5cf1671350 Make the takeover process more deterministic
Try to avoid submitting to be a follower while we are currently
becoming a leader
2018-07-20 13:50:42 -04:00
Jarrod Johnson e5c4219ee9 Reorder certificate check
First order of business is to verify certificate before even thinking
about if the request is possible
2018-07-20 13:34:14 -04:00
Jarrod Johnson 3ff7e42074 Change behavior for fallback handling
Fallback would do nothing to fix a persistent problem with an IPMI
session.  For lack of knowing how to avoid the situation, at least
make changes so it won't go wrong in the future.
2018-07-20 13:20:50 -04:00
Jarrod Johnson fab177e077 Fix node[group][attrib|define] handling of =
Attributes with = in the value were not handled correctly,
fix by only doing one split.
2018-07-20 09:54:17 -04:00
Jarrod Johnson a1ba5f59a8 Fix collective show on non-collective 2018-07-19 17:21:01 -04:00
Jarrod Johnson 9bcca6bfad Provide collective show on all members 2018-07-19 17:08:20 -04:00
Jarrod Johnson 96671ace4e Correct collective show behavior 2018-07-19 16:48:30 -04:00
Jarrod Johnson bcff3fc962 Improve collective show readability 2018-07-19 16:39:13 -04:00
Jarrod Johnson 54d93571d1 Have leader provide more data in collective show 2018-07-19 16:26:05 -04:00
Jarrod Johnson f2f902de7b Have collective show report when collective inactive
Collective show was misleading if not in a collective.
2018-07-19 15:59:15 -04:00
Jarrod Johnson a09792f969 Schedule periodic attempts to restart collective
If collective is lost due to connectivity, this will cause
occasional attempts to bring it back.
2018-07-19 15:49:05 -04:00
Jarrod Johnson 7d16c943a8 Handle updating address of collective member on connect
If a collective member changes its IP address, update at the next
possible opportunity.
2018-07-19 15:24:08 -04:00
Jarrod Johnson b053d41cd8 Error on loss of manager in flight 2018-07-19 14:36:23 -04:00
Jarrod Johnson 200569e7af Merge branch 'master' into clustertime 2018-07-19 13:32:00 -04:00
Jarrod Johnson c3c0e1570a Push quorum state to followers
The followers need to know quorum state.
2018-07-19 13:27:21 -04:00
Jarrod Johnson 10c82a72b5 Restore message on unreachable collective member
The parallel execution had broken how that message transmits.

Bonus, make it a per node error.
2018-07-18 16:49:54 -04:00
Jarrod Johnson 79cdf65a72 Fix SLES sockapi
Previous fix was applied to the incorrect section of code
2018-07-18 15:07:22 -04:00
Jarrod Johnson 497ca40492 Do not abort connecting process on bad cert
The target may be non-viable, but don't let that ruin the party
for everyone.  Let it keep going as if the system were down.
2018-07-18 14:58:16 -04:00
Jarrod Johnson fd33e6ae01 Fix non-collective confluent mode
list_collective returns an iterator, which will be True...
2018-07-18 14:53:23 -04:00
Jarrod Johnson 32f944e67c Handle unclean loss of current proxy host
If transition is less than gentle, provide a path to restore automatic
if it gets moved.
2018-07-18 14:32:39 -04:00
Jarrod Johnson dcad9f5a75 Add keepalive and acks to collective
Detect unplugged condition (eventually).
2018-07-18 13:45:03 -04:00
Jarrod Johnson 2a34388d09 Add -p to man page for nodepower 2018-07-18 11:02:12 -04:00
Jarrod Johnson 6993e0b496 Fix nodepower argument parsing
nodepower was assuming that the second parameter was always the
state regardless of option parsing.  Use args instead to fix.
2018-07-18 11:00:01 -04:00
Jarrod Johnson b7fe72673d Add clear node/group attributes to collective
collective was not syncing clear directives.
2018-07-17 15:57:48 -04:00
Jarrod Johnson 0159bf1b1d Fix typo in error message 2018-07-17 15:39:08 -04:00
Jarrod Johnson cf9ad11290 Short out operations if in collective mode but no collective.manager 2018-07-17 15:25:12 -04:00
Jarrod Johnson ddd7ef5eba Fix proxyconsole break and reopen 2018-07-17 15:05:09 -04:00
Jarrod Johnson 73da8ec8b5 Fix ProxyConsole if self.remote is not yet set 2018-07-17 14:44:59 -04:00
Jarrod Johnson eac4d97732 Disengage remote console on manager change
This results in a more direct treatment of manager change.
2018-07-17 13:10:01 -04:00
Jarrod Johnson fa9ecfbb94 Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-07-17 11:46:53 -04:00
Jarrod Johnson fc5472065a Catch missing '@' in token as invalid token 2018-07-17 11:46:40 -04:00
Jarrod Johnson cb0845596e Provide explanation about nodemedia list and no media. 2018-07-17 11:20:27 -04:00
Jarrod Johnson 0d936e0059 Ensure no more than one in-flight slave connection from a given follower
This will prevent a connection from deregistering itself after the
replacement registers itself.
2018-07-17 10:36:31 -04:00
Jarrod Johnson a7b8f0ab0c Parallelize cross-manager requests
Rather than doing it at one at a time, parallelize the requests
for improved performance.
2018-07-17 10:07:32 -04:00
Jarrod Johnson 3ab4203104 Explicitly set ECDHE curve
Some vintages of the SSL stack require we explicitly request a curve,
so here it is.
2018-07-16 16:23:33 -04:00
Jarrod Johnson 13aa2e9aae Catch more broad errors
Operating on a closed socket is not a socket.error
2018-07-16 11:58:18 -04:00
Jarrod Johnson 7462bc28e8 Use the eventlet socket in configmanager 2018-07-16 10:06:53 -04:00
Jarrod Johnson 18f1c07d65 Change to setting an errstr rather than exception
If nodefirmware update has an issue, provide error message instead.
2018-07-16 09:03:02 -04:00
Jarrod Johnson 0016077bee Ensure that wait_for_sync always does a new sync
If a sync is in progress, wait for that to complete.

Then issue the requested *new* sync.

Probably only needed if fullsync, as the one in progress may be a
'dirty' only sync and fullsync would be satisfied by the partial sync
without it, which is bad.
2018-07-13 22:15:38 -04:00
Jarrod Johnson 1dad69097b Be consistent with sync during load of leader cfg
Pass through sync as appropriate.

Also changes meant for previous commit
2018-07-13 21:52:17 -04:00
Jarrod Johnson fd7c428d1f Cleanup leftover sockets and more reliably be following or leading
Before there was a chance to be in a half state, leading to an inability
to reach consensus on leader.
2018-07-13 21:20:42 -04:00
Jarrod Johnson 80a1bd72e7 Correct arguments for Thread constructor 2018-07-13 15:43:09 -04:00
Jarrod Johnson 042d7ab5cf Modify clear_commit to use the same thread
Additionally, wrap a lock around the dbm operations, in case something
in the future makes a mistake.
2018-07-13 15:27:16 -04:00
Jarrod Johnson c74fdf5924 More collective join errors 2018-07-13 11:07:39 -04:00
Jarrod Johnson 58bf226d23 Relay error from server about token issue 2018-07-13 10:50:17 -04:00
Jarrod Johnson 6f012b69a1 Provide cleaner message for collective manager being unreachable 2018-07-13 10:43:20 -04:00
Jarrod Johnson 7f1e5d2302 Add explanation of 'all' in nodeattrib man page 2018-07-13 09:57:23 -04:00
Jarrod Johnson 3e2a827ff9 Correct typo in nodeattrib man page 2018-07-13 09:50:08 -04:00
Jarrod Johnson 1d16534c16 If replacing a follower stream, ensure the old one closes 2018-07-13 09:37:00 -04:00
Jarrod Johnson c80ebb0e8d Explicitly close connection before replacement
If an existing follower is stalled out, close the socket explicitly
to avoid leaving it open in lsof.
2018-07-13 09:14:36 -04:00
Jarrod Johnson efaf1dae70 Make cfgleader modifications more robust
If cfgleader is about to forget a socket, explicitly try to close
it first.
2018-07-13 09:05:28 -04:00
Jarrod Johnson 1de82936ed Add full sync mode
For implementing clear config, all data must be presumed dirty.
2018-07-12 17:06:37 -04:00
Jarrod Johnson b0c384c9ca Check quorum on attribute read
It's too bizarre for attribute read from api to work
without quorum, could be misleading.
2018-07-12 16:05:04 -04:00
Jarrod Johnson 61dd71778f Never generate new key on crypt read
An autogenerated key on read can never be useful.  Instead, let it fail
and assume a repair action is coming.
2018-07-12 15:55:05 -04:00
Jarrod Johnson 0f3014957b Fix non-ascii unicode handling of consoles 2018-07-12 14:16:44 -04:00
Jarrod Johnson c925353f02 Fix rollback
First the data was not actually being staged in the rollback area.
Secondly, there wasn't an assurance that the disk wouldn't have rollback
committed...
2018-07-12 10:01:32 -04:00
Jarrod Johnson 9edb225bd3 Fix the reference to sync to file 2018-07-12 09:20:41 -04:00
Jarrod Johnson 7cdc3c1400 Implement clear config rollback
Should something go awry during config
load, rollback the clear and load.
2018-07-12 08:48:21 -04:00
Jarrod Johnson bd2a3f14e6 Defer txcount increment until after potential failure
This avoids the txcount incrementing when no transaction actually
occurs.
2018-07-12 08:41:07 -04:00
Jarrod Johnson 87d00b7447 Fix typo in manpage for nodeattrib 2018-07-11 16:48:41 -04:00
Jarrod Johnson beedfb0600 If a drone doesn't exist, treat it as if it's an invalid certificate 2018-07-11 16:29:45 -04:00
Jarrod Johnson ce59a36351 Avoid excessive syncs on connect
This removes some redundancy and avoids writing and loading to disk
during the initialization process.
2018-07-11 16:07:56 -04:00
Jarrod Johnson a1e612968e Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-07-11 13:44:30 -04:00
Jarrod Johnson ce1a58bf58 Provide a more specific error when file doesn't exist
nodemedia does check locally, but server should also check it, in
case of remote *and* service nodes.
2018-07-11 13:17:37 -04:00
Jarrod Johnson b386084f2d Note future enhancement for flattening on collective manager change... 2018-07-11 11:07:59 -04:00
Jarrod Johnson 8e9bcbb44f Clear txcount on enroll
The transaction count on 'join' was being honored as high, when
it never should be.
2018-07-11 09:40:22 -04:00
Jarrod Johnson 704aaeecf9 Tolerate newline in myname
vim is quite insistent on adding a newline, tolerate that.
2018-07-11 09:36:51 -04:00
Jarrod Johnson 11968faffc Numerous fixes to collective
If client has higher transaction count, do not close the connection
before extracting peer address.

If our connect session is rudely terminated, abort rather than trying
to continue.

On assimilate failure, ignore a failed assimilate with no data.

Fix problem where a follower getting double deleted was causing an error.
2018-07-10 14:55:57 -04:00
Jarrod Johnson 8769c438c0 Relax cryptodomex requirement
We don't *need* the higher version, it just accelerates password auth
if possible.  Fix conflict with RH provided package.
2018-07-10 10:10:04 -04:00
Jarrod Johnson f1489bf527 Add all to SYNOPSIS of nodeattrib 2018-07-09 16:52:07 -04:00
Jarrod Johnson c03781c022 Add 'all' to usage message of nodeattrib 2018-07-09 16:49:45 -04:00
Jarrod Johnson 298e11f60f Allow invite from non-leader role
A non-leader transaction is modified such that the enroll node
can be connected to the leader and have validation.
2018-07-09 16:40:43 -04:00
Jarrod Johnson 67d6e9a6c7 Add collective show
Provide a harmless way to look at collective state
2018-07-09 15:07:24 -04:00
Jarrod Johnson a4edf9afb8 Rename confluentutil to collective
Also adjust output to be a bit more automation friendly.
2018-07-09 13:33:56 -04:00
Jarrod Johnson 2342fe717e Remove superfluous call to sync to file
load_from_json already makes the call, remove the extra call that is
redundant.
2018-07-09 12:59:37 -04:00
Jarrod Johnson 08cf698609 Only conditionally require ffi
Only collective mode requires ffi, do not incur requirement for
non-collective mode.
2018-07-09 09:41:10 -04:00
Jarrod Johnson a905ae4865 Only zap cfgleader if not set
stale relay_slaved_requests was trouncing following status, correct
by avoiding zapping that state
2018-07-03 14:43:23 -04:00
Jarrod Johnson 1eaf5357ca Resolve race conditions on simultaneous collective outage
Implement random backoff strategy for serializing connect out and
connect in.
2018-07-03 14:09:09 -04:00
Jarrod Johnson 39378170b1 Add an error if cfgstreams loses a follower 2018-07-03 11:13:23 -04:00
Jarrod Johnson 2156e99ae4 Use alternative name for pycryptodomex 2018-07-03 10:43:05 -04:00
Jarrod Johnson bba12ed9e7 Update requirements to pull in cryptodomex 2018-07-03 10:32:33 -04:00
Jarrod Johnson 282043ed97 Switch to cryptodome
Cryptodome is a modern, but compatible replacement for pycrypto.

We may move to cryptography eventually, but start with this for now
for some nice speedups in some cases.
2018-07-03 10:31:13 -04:00
Jarrod Johnson 09c239b294 Fix memory leaks
For one, configmanager was left with stale callback references, clean
those up.

For another, the callback pattern was creating a circular reference that
python memory management couldn't overcome.  Break the reference
explicity when an item is disposed of.
2018-07-03 08:58:32 -04:00
Jarrod Johnson 0b26d12837 Fix memory leaks
For one, configmanager was left with stale callback references, clean
those up.

For another, the callback pattern was creating a circular reference that
python memory management couldn't overcome.  Break the reference
explicity when an item is disposed of.
2018-07-03 08:58:11 -04:00
Jarrod Johnson 956faee052 Correct typo in variable name 2018-06-28 14:12:56 -04:00
Jarrod Johnson 8e77466875 Carry txcount through connect replica
The txcount was not up to date when offline updates occurred.
2018-06-28 13:54:31 -04:00
Jarrod Johnson f01c7e19f7 Fix HA healing problems
If we are superior to leader, become leader
When abandoned by a leader, forget the leader.
2018-06-28 13:51:52 -04:00
Jarrod Johnson 5d894912ac Clean up more potential for CLOSE_WAIT
Explicitly close sockets in a few more places.
2018-06-28 10:50:03 -04:00
Jarrod Johnson 93e78d1f03 Fix try_assimilate
Also get rid of CLOSE_WAIT in some situations.
2018-06-28 09:53:34 -04:00
Jarrod Johnson 97f932b946 Fix the attempt to assimilate
In the wake of leader loss, the assimilate attempt was causing one
of a couple of bad behaviors, a trace if broken and if it should
work, the format of the data payload was incorrect.
2018-06-26 15:29:51 -04:00
Jarrod Johnson 61f24f4d8a Fix mismatched braces in previous commit 2018-06-26 15:09:17 -04:00
Jarrod Johnson 68a79695fc Add check for quorum to invite generation process
If a would-be collective member has no quorum, provide a clear
message indicating this as an issue.
2018-06-26 14:55:42 -04:00
Jarrod Johnson 401352998c Correctly show the error on non-leader
When non-leader tries to invite, print the error rather than unhelpful
exception with no helpful data.
2018-06-26 14:35:23 -04:00
Jarrod Johnson 9eeede651e Error when inviting from non leader 2018-06-26 14:24:52 -04:00
Jarrod Johnson fb98dd5636 Fix packaging issues 2018-06-26 14:12:53 -04:00
Jarrod Johnson 0843b991ea Isolate following to dedicated greenthread and put fallback to assert leadership 2018-06-26 14:06:01 -04:00
Jarrod Johnson 11e6145a46 Add quorum check to non-config requests 2018-06-26 13:52:20 -04:00
Jarrod Johnson 7433dd3e38 Wrap cfg init on follow in lock
Use a lock to provide more atomic behavior for connecting should
something go wrong in calling connect_to_leader incorrectly.
2018-06-26 10:13:27 -04:00
Jarrod Johnson 4de1fea7aa Implement attempt to assimilate 2018-06-25 17:30:29 -04:00
Jarrod Johnson f6342dd31f Allow connect_to_leader to cycle in the parent for loop
Startup was foiled when one entry was bad.  Also add comments
on invite/join needing to be done from current leader, and the need
for better error messages.
2018-06-25 14:51:39 -04:00
Jarrod Johnson 0499a14fe8 Try to reestablish collective on leader loss 2018-06-25 10:40:29 -04:00
Jarrod Johnson f301cd272f Have follower notice lost leader
This will mitigate period during which requests
hang instead of error.
2018-06-24 11:08:15 -04:00
Jarrod Johnson bf4f5ad5ae Recognize loss of follower as step toward loss of quorum
Properly reap the loss of a follower.
2018-06-22 16:17:48 -04:00
Jarrod Johnson 5a5f0169a7 Fix logic on null messages 2018-06-22 15:46:41 -04:00
Jarrod Johnson 03adec089d Also clear cfgleader when actually becoming leader 2018-06-22 15:26:08 -04:00
Jarrod Johnson f065fafd61 Clear the quorum block on initial xfer
The initial transfer needs to have the configmanager working locally.
2018-06-22 15:23:20 -04:00
Jarrod Johnson b315042850 Error if starting without quorum
If collective mode is present, but no candidates worked, still error
out.
2018-06-22 15:12:42 -04:00
Jarrod Johnson 5588712320 Clear transaction count on config clear
A clear configuration should have 0 transactions.
2018-06-22 14:46:52 -04:00
Jarrod Johnson c6a0aeca3b Fix dispatch of commands with InputData
Inputdata needed to be serialized for the network.  Further, had
to have a JSON-safe payload for indicating name for certificate look
up, to avoid doing pickle load on client input prior to client
validation.
2018-06-22 14:41:41 -04:00
Jarrod Johnson f45228c067 Auto-migrate open console session on loss of management
This will keep consoles connected across a manager change.  Note
that as it stands, a console session enduring multiple changes will
become increasingly convoluted as the redirect chain will increase
every time, rather than shorting back to the entry point.
2018-06-21 15:14:56 -04:00
Jarrod Johnson d34e65f9b7 Fix tlvdata handling of unicode input
Unicode input is normalized to bytes.  Also have to handle python3
not having 'unicode', do a quick change to support that in both.
2018-06-21 14:29:54 -04:00
Jarrod Johnson baef8c2c42 Connect and fix the proxyconsole
This works full duplex for sockapi and read for web consoles.
2018-06-21 13:46:51 -04:00
Jarrod Johnson 1543e145b7 Draft for proxyconsole object for remote use of consoles
This would be the stub stand in for the console object to
connect to remote console object rather than local.
2018-06-20 16:36:59 -04:00
Jarrod Johnson 2d403f7d68 Defer import of confluent log
confluent.util pulls in log more cleanly, for now rearrange for
easier time running slp test mode.
2018-06-20 14:56:07 -04:00
Jarrod Johnson 78117a1b1a Draft proxyconsole support for sockapi
Foundation for consoleserver to be able to do backend to backend
connections
2018-06-19 16:50:49 -04:00
Jarrod Johnson 624012774c Discontinue processing on cert mismatch
If cert mismatched, the client was shut out, but processing continued,
fix this mistake.
2018-06-19 16:45:06 -04:00
Jarrod Johnson 003358da5f Discontinue ipmi session on collective manager change 2018-06-19 15:10:59 -04:00
Jarrod Johnson 6b24b9691b Merge branch 'master' into clustertime 2018-06-19 11:07:53 -04:00
Jarrod Johnson 6ba8ca2fa2 Remove accidental change
keepalive was disabled, which negatively impacted
web ui performance.  Re-enable.
2018-06-19 11:07:23 -04:00
Jarrod Johnson 38898ca921 Auto-make certificate if missing
Automatically fix a missing certificate if this is the case.
2018-06-19 11:05:38 -04:00
Jarrod Johnson 810be71720 Initial support for non-console dispatch
For non-exceptional cases, it is now functional.
2018-06-15 15:54:26 -04:00
Jarrod Johnson b877a95645 Include absent devices in the json of nodeinventory 2018-06-15 11:03:03 -04:00
Jarrod Johnson efd5732682 Amend json output
Have the nodeinventory json output in a bit more directly useful format,
rather than regarding the API structured JSON...
2018-06-15 11:02:57 -04:00
Jarrod Johnson 4906d6e9c4 Add --json to nodeinventory
Have nodeinventory have an option to output in json.
2018-06-15 11:02:51 -04:00
Jarrod Johnson f2500d9d27 Add general confluentutil command
This provides util commands to manage certificates and collective
membership.
2018-06-13 16:23:49 -04:00
Jarrod Johnson 0507e89da8 Add ability to skip key backup and interactive password
Backups should carefully protect keys.json, but that's only feasible
interactively.  However keys don't change, so have a way to combine
protected keys.json with password with relatively safe non-interactive
incremental backups.
2018-06-13 16:22:40 -04:00
Jarrod Johnson 8cea5a4fed Fix redacted db dump
The db dump function did not trigger the init() function.
Rearrange the collective section to occur after a section that does
ensure the configmanager is initted.
2018-06-13 09:15:27 -04:00
Jarrod Johnson 8515d43dad A shell script to illustrate generating ECDSA key
For now put down the openssl commands required to get the key and
certificate available.
2018-06-12 16:57:36 -04:00
Jarrod Johnson 5c12dc2cba Do not require exactly TLSv1.0
This was breaking TLSv1.2.
2018-06-08 10:15:38 -04:00
Jarrod Johnson a08eace00c Fix missing import of traceback 2018-06-05 13:08:05 -04:00
Jarrod Johnson cdb20c0302 Fix missing import of traceback 2018-06-05 13:05:42 -04:00
Jarrod Johnson 3eed46ec08 Disconnect on loss of ownership 2018-06-02 12:36:20 -04:00
Jarrod Johnson daef9fa60b Fix confusing nodeconfig error handling
Properly react to error conditions
2018-06-01 16:48:34 -04:00
Jarrod Johnson a7a4ede580 Fix confusing nodeconfig error handling
Properly react to error conditions
2018-06-01 16:48:19 -04:00
Jarrod Johnson a560dc1974 Add timeout on httpapi socket
Clients that fail to send any data, or keep a persistent socket
open without using it are killed off.
2018-06-01 16:26:21 -04:00
Jarrod Johnson e8cea66a85 Add timeout on httpapi socket
Clients that fail to send any data, or keep a persistent socket
open without using it are killed off.
2018-06-01 16:25:38 -04:00
Jarrod Johnson 8fc29d1b46 Correctly allow manual discovery through ambiguous situation
Manual discovery may 'catch' some incidental data from auto-discovery.
Since the operation is manual, trust the user rather than assume the
user is confused.
2018-06-01 16:09:32 -04:00
Jarrod Johnson 6e0186947f Correctly allow manual discovery through ambiguous situation
Manual discovery may 'catch' some incidental data from auto-discovery.
Since the operation is manual, trust the user rather than assume the
user is confused.
2018-06-01 16:09:13 -04:00
Jarrod Johnson a45a0e31f1 Merge branch 'master' of github.com:jjohnson42/confluent 2018-06-01 15:54:31 -04:00
Jarrod Johnson 8ea532053e Add guardrail around slp snoop
If a program error should befall our poor slp service, log the issue
and carry on.
2018-06-01 15:54:27 -04:00
Jarrod Johnson 366c955956 Add guardrail around slp snoop
If a program error should befall our poor slp service, log the issue
and carry on.
2018-06-01 15:53:52 -04:00
Jarrod Johnson d968fb6b04 Add TODO note on how to do the collective console sessions
We will hove consoleserver abstract this away from sockapi/httpapi
concerns.
2018-05-31 16:22:11 -04:00
Jarrod Johnson db1d5d6dff Prepare for request dispatch
Sort and exclude nodes that have collective.manager, prepare to relay
for dispatch.
2018-05-31 16:18:58 -04:00
Jarrod Johnson 867dd0dda7 Handle collective.manager being set back to the node 2018-05-31 16:16:03 -04:00
Jarrod Johnson 22b63df036 First pass at suppressing foreign consoles
If console session is another node, prevent it from working locally.
Focus first on making sure the wrong nodes don't work before routing.
2018-05-31 15:53:09 -04:00
Jarrod Johnson 54f419d9b4 Add 'collective.manager'
For requests that must be routed to a definitive owner (e.g. IPMI),
have an attribute to track the currently responsible server for
singleton operations.
2018-05-31 15:47:19 -04:00
Jarrod Johnson 5d6e241287 Fix deadlock on collective create user
There was a call to rpc-out from within a 'true' function, which is bad,
fix this by only calling the 'true' function from 'true' function.
2018-05-30 16:21:40 -04:00
Jarrod Johnson 417bc5acda Fix incorrect function call for user creation
In collective mode, the incorrect rpc call was made
2018-05-30 15:30:27 -04:00
Jarrod Johnson 57ffe166d3 Merge branch 'master' into clustertime 2018-05-25 10:24:34 -04:00
Jarrod Johnson 5718c60a51 Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-05-25 10:23:25 -04:00
Jarrod Johnson 31effcc025 Fix mistake in variable name in nodeconfig 2018-05-25 10:22:35 -04:00
Jarrod Johnson cefca49128 Fix mistake in variable name in nodeconfig 2018-05-25 10:21:34 -04:00
Jarrod Johnson 41a5eaa464 Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-05-23 13:02:12 -04:00
Jarrod Johnson 46c4065b81 Correct another typo 2018-05-23 13:02:03 -04:00
Jarrod Johnson 57e323786e Fix syntax error in the recent code 2018-05-22 11:00:30 -04:00
Jarrod Johnson 3b2a18a650 Merge branch 'master' into clustertime 2018-05-22 10:34:31 -04:00
Jarrod Johnson 3ace7747ab Fix typo 2018-05-22 10:11:37 -04:00
Jarrod Johnson 8ede0fd8ef Document {{}} escape on noderun and nodeshell
Documentation did not explain that
2018-05-22 09:59:30 -04:00
Jarrod Johnson be3ecf60a5 Fix bad error message on {} in nodeshell/noderun
{} used in awk is likely, give proper error message.
2018-05-22 09:56:53 -04:00
Jarrod Johnson 8807fcfd22 Fix missing portname in lldp data
Root cause was pysnmp returning extraneous leftover data causing
calling code to overrite good data.
2018-05-22 09:37:01 -04:00
Jarrod Johnson 33fe0a3db4 Fix wrong port name for G8332
Was using the incorrect half of the return, which broke on G8332.
2018-05-22 09:36:55 -04:00
Jarrod Johnson ca7711b373 Fix missing portname in lldp data
Root cause was pysnmp returning extraneous leftover data causing
calling code to overrite good data.
2018-05-22 09:36:09 -04:00
Jarrod Johnson 8b37199654 Fix wrong port name for G8332
Was using the incorrect half of the return, which broke on G8332.
2018-05-22 09:34:02 -04:00
Jarrod Johnson ff2bc89fae Merge branch 'master' into clustertime 2018-05-21 15:53:48 -04:00
Jarrod Johnson 5fe2d2a31c Fix unprintable characters in some chassisid
Some switches send raw octets back, some printable.  Try to normalize
when unprintable chassis id are detected.  This is not 100%, if the hex
would be all between 20 and 80 throughout the string, then this will
fail to do the right thing.

Hopefully, the amount of times when lldp partners disagree on how to
implement LLDP-MIB will be limited.  Currently it is known than Lenovo
and Juniper switches disagree, and both of those have what would
be unprintable values in the mfg portion of the chassis id.
2018-05-21 15:53:42 -04:00
Jarrod Johnson a4fed0601c Fix unprintable characters in some chassisid
Some switches send raw octets back, some printable.  Try to normalize
when unprintable chassis id are detected.  This is not 100%, if the hex
would be all between 20 and 80 throughout the string, then this will
fail to do the right thing.

Hopefully, the amount of times when lldp partners disagree on how to
implement LLDP-MIB will be limited.  Currently it is known than Lenovo
and Juniper switches disagree, and both of those have what would
be unprintable values in the mfg portion of the chassis id.
2018-05-21 15:53:12 -04:00
Jarrod Johnson 41298a8e01 Extend collective data functions to more functions
Add to users and groups.  Refactor reusable code.
Code that remains still looks awfully repetitive though...
2018-05-21 15:46:51 -04:00
Jarrod Johnson caa4000b7e Merge branch 'master' into clustertime 2018-05-21 11:49:19 -04:00
Jarrod Johnson fde2c7a8e0 Fix the encuuid reference
encuuid is a list, not the value, so get the first value
rather than try to concatenate the string.
2018-05-18 11:49:06 -04:00
Jarrod Johnson fbbb5d048f Fix the encuuid reference
encuuid is a list, not the value, so get the first value
rather than try to concatenate the string.
2018-05-18 11:47:34 -04:00
Jarrod Johnson 32d60145f7 Fix typo in discovery core 2018-05-18 10:20:57 -04:00
Jarrod Johnson 1db781852c Fix typo in discovery core 2018-05-18 10:20:31 -04:00
Jarrod Johnson 0dbf82b0f1 Clean up errors on bad ipv4 addresses
confluent errors are better now
2018-05-17 16:24:31 -04:00
Jarrod Johnson 675dc966c7 Clean up errors on bad ipv4 addresses
confluent errors are better now
2018-05-17 16:24:06 -04:00
Jarrod Johnson a9485706d1 Update warning to be commented out, just in case.. 2018-05-17 15:40:59 -04:00
Jarrod Johnson db1ae03415 Sample script for mac to ipv6 translation
Useful for some generic applications where nodediscover
does not have full support, but must be used with care
as it doesn't guarantee the mac address is what we expect
it to be.
2018-05-17 15:40:49 -04:00
Jarrod Johnson 9826235d4d Update warning to be commented out, just in case.. 2018-05-17 15:40:20 -04:00
Jarrod Johnson 232140899e Sample script for mac to ipv6 translation
Useful for some generic applications where nodediscover
does not have full support, but must be used with care
as it doesn't guarantee the mac address is what we expect
it to be.
2018-05-17 15:35:52 -04:00
Jarrod Johnson 5dddae0ebf Cleaner handling of invalid names in restore attempt
Detect problems ahead af time and more cleanly print a message.
2018-05-17 14:40:40 -04:00
Jarrod Johnson 39e9bf0be5 Cleaner handling of invalid names in restore attempt
Detect problems ahead af time and more cleanly print a message.
2018-05-17 14:40:19 -04:00
Jarrod Johnson d6b7c536d5 Fix discovery of old SMM firmware
Older SMM firmware will not have neighbor data, ignore and move on
in such a case.
2018-05-17 14:21:24 -04:00
Jarrod Johnson f21db46cdd Fix discovery of old SMM firmware
Older SMM firmware will not have neighbor data, ignore and move on
in such a case.
2018-05-17 14:20:59 -04:00
Jarrod Johnson 2d1ba7cc9b Merge branch 'master' into clustertime 2018-05-17 13:13:46 -04:00
Jarrod Johnson 22049002bb Fix exitcode references before use 2018-05-17 11:11:11 -04:00
Jarrod Johnson 727aa9a56c Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-05-16 11:28:08 -04:00
Jarrod Johnson dcb1c2b32b Fix load of txcount
Mistake caused txcount not to restore from disk.
2018-05-16 11:27:46 -04:00
Jarrod Johnson d705d6320a Start setting the stage for leader change on restart
Have connect() have a way to recover if leader is dead.

Also these will be involved in configmanager detected loss of leader
2018-05-16 11:27:46 -04:00
Jarrod Johnson 52e2038fdf Fix transaction count in collective
Slave members were not persisting the value to disk
2018-05-16 11:27:46 -04:00
Jarrod Johnson 9d58a2d382 Correct scope of currentleader 2018-05-16 11:27:46 -04:00
Jarrod Johnson a2187087f7 Fix not having currentleader set
A slave node now recognizes itself as such.
2018-05-16 11:27:46 -04:00
Jarrod Johnson c5b5178f39 Block some early startup problems in collective 2018-05-16 11:27:46 -04:00
Jarrod Johnson ff026ee034 Include absent devices in the json of nodeinventory 2018-05-16 11:27:46 -04:00
Jarrod Johnson 1cc659a3b0 Amend json output
Have the nodeinventory json output in a bit more directly useful format,
rather than regarding the API structured JSON...
2018-05-16 11:27:46 -04:00
Jarrod Johnson 8bc8faf0bc Add --json to nodeinventory
Have nodeinventory have an option to output in json.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 1c2c9931a8 Persist the transactioncount
Needed for eventually ascertaining the viability in selecting leader.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 778a153170 Correct spelling error 2018-05-16 11:27:46 -04:00
Jarrod Johnson 34c510e30a Try to persist name as myname
hostname may not agree with the name chosen by user, in such a case
persist the name and use that, falling back to gethostname()
as needed.
2018-05-16 11:27:46 -04:00
Jarrod Johnson d4babbffa4 Check and try to start collective on startup
Not yet good enough for a leader to rejoin, but enough for a follower
to rejoin automatically.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 1c930eba9d Have attrib set wait on all collective members
This will mean that it is reliable that a nodeattrib ; <command>
in delegation scenarios is guaranteed to execute in order.
2018-05-16 11:27:46 -04:00
Jarrod Johnson c4b564123f Allow slave collective drones to set
It works (once), but needs xid fix.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 6d728df4dc Fix reuse of channel for receiving changes 2018-05-16 11:27:46 -04:00
Jarrod Johnson f1e29393df Succeed in pushing config to followers from leader
Still more work to be done for multiple transactions.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 81bb16476c Apply changes from leader subscription 2018-05-16 11:27:46 -04:00
Jarrod Johnson 035f10e7d7 Rough draft for ongoing syncronization
Putting thoughts down on how xmit will work, will add recv and relay,
do some testing, and then decide how much can be done to apply it neatly
to the various points.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 830e6bb4e4 Clear configuration prior to sync 2018-05-16 11:27:46 -04:00
Jarrod Johnson 1eb542f6a8 Actually execute replicate-on-connect
This creates a duplicate of the leader.
2018-05-16 11:27:46 -04:00
Jarrod Johnson b733049a0c Add self to collective database
Database would omit initial leader otherwise.
2018-05-16 11:27:46 -04:00
Jarrod Johnson a4d80e4e3a Fixes to the connect draft
Needed to track it's own name, skip the banner and auth message...
2018-05-16 11:27:46 -04:00
Jarrod Johnson a69b5fbb50 For enroll, track the remote cert special
For reconnect, we will have collective objects.  However at
enroll time, we need to be special.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 789dfe94d0 Fix missing import of eventlet
Unable to spawn the connect thread due to missing import.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 73a376fd74 Fix backup of globals
Globals failed to open the backup file as writable, causing failure if
a global had been set.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 57f390fd0a Draft for starting the databse replication
Does not actually heed the data, or implement ongoing relay of data back and forth.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 641bc7344a Add hooks for collective mode and refactor
In support of config replication, need configmanager to do a few things
2018-05-16 11:27:46 -04:00
Jarrod Johnson af940c972f Add function to check address equivalence
As we start needing to compare addresses, provide a central function
to handle the various oddities associated with that.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 468f00cd1c Rename 'joinchallenge' to 'enroll'
Seems like a better word to use.
2018-05-16 11:27:46 -04:00
Jarrod Johnson ce635068aa Begin the 'connect' collective operation
First check if we are current leader, reject if not, then if cert
is invalid, reject, then comes the TODO.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 348c6a7c38 Add collective info to DB backup
Now persisted to disk *and* accessible to backup.
2018-05-16 11:27:46 -04:00
Jarrod Johnson a41a42ffd0 Persint collective info to disk
Additionally, simplify the concluding steps of the join conversation.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 3a354a6300 Ensure the invitation works out to even multiple of 3 bytes
It's cosmetic, but a nice way to avoid '=' in the tokens.
2018-05-16 11:27:46 -04:00
Jarrod Johnson fb9925bb84 Fix encoding of the response proof
The response was not decoded, causing it to always fail.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 78bf1d5acd Provide more feedback and fix some flow issues 2018-05-16 11:27:46 -04:00
Jarrod Johnson 8165b645d9 Fix invite process and unicode
Unicode strings do not fit with our world view, make them bytes.
2018-05-16 11:27:46 -04:00
Jarrod Johnson c2783b6734 Rename swarm to collective in setup.py.tmpl 2018-05-16 11:27:46 -04:00
Jarrod Johnson 033d59b04a Afetr some feedback, rename it 'collective' 2018-05-16 11:27:46 -04:00
Jarrod Johnson 4155954d1c Add swarm to setup.py
Make sure the swarm content is actually installed.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 1b912c4365 Further advance the swarm concept
This marks the start of attempting to connect the invitation
to sockets and using the invitation to measure the certificates as
well as proving client knowledge of an invitation token.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 5f9ee3d3c5 Migrate 'multimanager' to 'swarm'
It's easier to say 'swarm' and conveys the sense without confusion
of 'cluster' mode.
2018-05-16 11:27:46 -04:00
Jarrod Johnson cc9becea3b Add ability to get client certificates
Unfortunately, to pull off the target user experience, we
must register a custom client certificate validation to allow
us to not require a CA.
2018-05-16 11:27:46 -04:00
Jarrod Johnson 1e0cf7e9fb Create invitation management module
This facilitates the generation of invitations and logistics of proving
knowledge of the invitation and the integrity of the certificates.
peercert is to be gotten through getpeercert(binary_form=True) and
local cert through the util function to load from file, since we don't
have another way of getting local certificate.
2018-05-16 11:26:36 -04:00
Jarrod Johnson 7ebe9da24b Add utility function to get certificate from file
This can be used to get our own certificate, for use in the
multimanager membership establishment.
2018-05-16 11:26:36 -04:00
Jarrod Johnson 6cba560f6a Fix nodeconfig handling of general errors
nodeconfig was not handling errors in results well, fix this by
refactoring the nodefirmware facility into it.
2018-05-16 11:21:26 -04:00
Jarrod Johnson 08dcab4c72 Fix load of txcount
Mistake caused txcount not to restore from disk.
2018-05-14 16:25:27 -04:00
Jarrod Johnson 4f73ddc41e Start setting the stage for leader change on restart
Have connect() have a way to recover if leader is dead.

Also these will be involved in configmanager detected loss of leader
2018-05-14 16:22:27 -04:00
Jarrod Johnson 7a912b31cb Fix transaction count in collective
Slave members were not persisting the value to disk
2018-05-14 15:35:44 -04:00
Jarrod Johnson 297513bba7 Correct scope of currentleader 2018-05-11 15:03:41 -04:00
Jarrod Johnson aa2be98dc3 Fix not having currentleader set
A slave node now recognizes itself as such.
2018-05-11 15:00:18 -04:00
Jarrod Johnson 3cc9ee1a17 Block some early startup problems in collective 2018-05-11 14:49:46 -04:00
Jarrod Johnson 173f1eaf7e Include absent devices in the json of nodeinventory 2018-05-11 14:24:47 -04:00
Jarrod Johnson 0d4b1a4213 Amend json output
Have the nodeinventory json output in a bit more directly useful format,
rather than regarding the API structured JSON...
2018-05-11 14:18:04 -04:00
Jarrod Johnson 58155a47b5 Add --json to nodeinventory
Have nodeinventory have an option to output in json.
2018-05-11 13:49:54 -04:00
Jarrod Johnson 7fa431dbc9 Persist the transactioncount
Needed for eventually ascertaining the viability in selecting leader.
2018-05-11 11:53:45 -04:00
Jarrod Johnson e98ecd9867 Correct spelling error 2018-05-10 16:46:29 -04:00
Jarrod Johnson 5087c8bed5 Try to persist name as myname
hostname may not agree with the name chosen by user, in such a case
persist the name and use that, falling back to gethostname()
as needed.
2018-05-10 16:40:13 -04:00
Jarrod Johnson 0477ab7d85 Check and try to start collective on startup
Not yet good enough for a leader to rejoin, but enough for a follower
to rejoin automatically.
2018-05-10 16:19:46 -04:00
Jarrod Johnson 267d83e6e4 Have attrib set wait on all collective members
This will mean that it is reliable that a nodeattrib ; <command>
in delegation scenarios is guaranteed to execute in order.
2018-05-09 17:01:23 -04:00
Jarrod Johnson c962d10222 Allow slave collective drones to set
It works (once), but needs xid fix.
2018-05-08 16:53:59 -04:00
Jarrod Johnson e5f553801b Fix reuse of channel for receiving changes 2018-05-08 13:35:30 -04:00
Jarrod Johnson d11c716b6a Succeed in pushing config to followers from leader
Still more work to be done for multiple transactions.
2018-05-08 11:33:42 -04:00
Jarrod Johnson aec4e746e9 Apply changes from leader subscription 2018-05-04 16:12:53 -04:00
Jarrod Johnson a78aa6816c Rough draft for ongoing syncronization
Putting thoughts down on how xmit will work, will add recv and relay,
do some testing, and then decide how much can be done to apply it neatly
to the various points.
2018-05-04 15:16:30 -04:00
Jarrod Johnson 5abaddfe63 Clear configuration prior to sync 2018-05-04 12:19:45 -04:00
Jarrod Johnson ecfc56efde Actually execute replicate-on-connect
This creates a duplicate of the leader.
2018-05-03 14:03:56 -04:00
Jarrod Johnson ecfb4d68c5 Add self to collective database
Database would omit initial leader otherwise.
2018-05-03 13:27:48 -04:00
Jarrod Johnson 855241a043 Fixes to the connect draft
Needed to track it's own name, skip the banner and auth message...
2018-05-03 13:18:08 -04:00
Jarrod Johnson ce4c72eae2 For enroll, track the remote cert special
For reconnect, we will have collective objects.  However at
enroll time, we need to be special.
2018-05-03 11:18:02 -04:00
Jarrod Johnson c8961377ed Fix missing import of eventlet
Unable to spawn the connect thread due to missing import.
2018-05-01 16:51:57 -04:00
Jarrod Johnson 1b17c42cae Fix backup of globals
Globals failed to open the backup file as writable, causing failure if
a global had been set.
2018-05-01 16:18:31 -04:00
Jarrod Johnson 196e8d0d58 Draft for starting the databse replication
Does not actually heed the data, or implement ongoing relay of data back and forth.
2018-04-30 16:20:49 -04:00
Jarrod Johnson dbb50f0807 Add hooks for collective mode and refactor
In support of config replication, need configmanager to do a few things
2018-04-30 16:09:28 -04:00
Jarrod Johnson 1200f7b7a1 Add function to check address equivalence
As we start needing to compare addresses, provide a central function
to handle the various oddities associated with that.
2018-04-30 16:08:15 -04:00
Jarrod Johnson a2a0b5de2c Rename 'joinchallenge' to 'enroll'
Seems like a better word to use.
2018-04-30 11:37:23 -04:00
Jarrod Johnson c7b01e00b6 Begin the 'connect' collective operation
First check if we are current leader, reject if not, then if cert
is invalid, reject, then comes the TODO.
2018-04-26 16:34:54 -04:00
Jarrod Johnson 06fdc648b8 Add collective info to DB backup
Now persisted to disk *and* accessible to backup.
2018-04-26 16:03:03 -04:00
Jarrod Johnson de89803b9c Persint collective info to disk
Additionally, simplify the concluding steps of the join conversation.
2018-04-26 15:48:15 -04:00
Jarrod Johnson d38d9204a7 Ensure the invitation works out to even multiple of 3 bytes
It's cosmetic, but a nice way to avoid '=' in the tokens.
2018-04-26 11:35:06 -04:00
Jarrod Johnson 1deb44021f Fix encoding of the response proof
The response was not decoded, causing it to always fail.
2018-04-25 20:49:08 -04:00
Jarrod Johnson 619bbbca96 Provide more feedback and fix some flow issues 2018-04-25 16:47:42 -04:00
Jarrod Johnson 8246ebdd2b Fix invite process and unicode
Unicode strings do not fit with our world view, make them bytes.
2018-04-25 14:21:12 -04:00
Jarrod Johnson a94a724fe0 Rename swarm to collective in setup.py.tmpl 2018-04-25 13:31:26 -04:00
Jarrod Johnson 6b9aed3722 Afetr some feedback, rename it 'collective' 2018-04-25 13:01:59 -04:00
Jarrod Johnson a3de8b9374 Add swarm to setup.py
Make sure the swarm content is actually installed.
2018-04-24 15:58:40 -04:00
Jarrod Johnson c8e5808daf Further advance the swarm concept
This marks the start of attempting to connect the invitation
to sockets and using the invitation to measure the certificates as
well as proving client knowledge of an invitation token.
2018-04-24 14:44:45 -04:00
Jarrod Johnson f3a0ccbff8 Migrate 'multimanager' to 'swarm'
It's easier to say 'swarm' and conveys the sense without confusion
of 'cluster' mode.
2018-04-24 12:59:24 -04:00
Jarrod Johnson 27c1355a4f Add ability to get client certificates
Unfortunately, to pull off the target user experience, we
must register a custom client certificate validation to allow
us to not require a CA.
2018-04-24 11:00:23 -04:00
Jarrod Johnson 7909f9e003 Switch to explicit SSL context when possible
This allows more fine grained control over the security parameters of
the TLS connection.
2018-04-23 14:18:51 -04:00
Jarrod Johnson 97d38efb29 Merge branch 'master' into clustertime 2018-04-23 11:13:22 -04:00
Jarrod Johnson 14ff33a44a Only activate the remote API socket if user makes cert
This prevents the useless networking socket from being opened
when it cannot be used.  This means most implementations will not
have an extra port to explain unless the user goes through the work
and knows what it would be.
2018-04-20 19:30:15 -04:00
Jarrod Johnson 78bdac474f Merge branch 'master' into clustertime 2018-04-20 13:25:16 -04:00
Jarrod Johnson 0481f7889b Make macmap api case insensitive
This helps usability of the api.
2018-04-20 13:25:02 -04:00
Jarrod Johnson 123a1a9dc1 Merge branch 'clustertime' of github.com:jjohnson42/confluent into clustertime 2018-04-17 11:10:13 -04:00
Jarrod Johnson fa24622704 Merge branch 'master' into clustertime 2018-04-17 11:09:59 -04:00
Jarrod Johnson a1156097d2 Add facility to disable autosense
discovery autosense at scale may produce undesirable performance.
Provide an interface to turn off the autosense.

If autosense is off, manual scan can still be performed.
2018-04-13 16:54:27 -04:00
Jarrod Johnson af72d0e71a Update the discovery lookup tables on node add/remove
This will mitigate stale mappings in the discovery process.
2018-04-12 17:05:06 -04:00
Jarrod Johnson 008f8e22ae Abort traversing gap in SMM chain
Once there is a gap, the next hop in the chain will be ambiguous.
Discovery must always precede from the front-most chassis.
2018-04-12 15:45:07 -04:00
Jarrod Johnson 39ee0da879 Fix makesetup for confluent_client
Fixing the redundant __init__.py led to no __init__.py, fix
that mistake.
2018-04-10 16:11:14 -04:00
Jarrod Johnson fc7b26eaf7 Remove __init__.py from tracking in client 2018-04-10 16:09:26 -04:00
Jarrod Johnson 91238f1dcb Clean up pure python packaging
Fix __init__.py redundancy, update requirements to current state
of affairs.
2018-04-10 16:06:37 -04:00
Jarrod Johnson 7f29d3a48f Merge branch 'master' into clustertime 2018-04-10 15:11:59 -04:00
Jarrod Johnson 76a4a91351 Fix pyparsing rpm name
Accept another likely formulation of an rpm name for
the package.
2018-04-10 15:11:20 -04:00
Jarrod Johnson 5ca52ff03b Handle interruptions to select such as resize
Resize can cause an interrupted operation on stdin, handle that.
2018-04-09 10:48:06 -04:00
Jarrod Johnson bd40f2f4a6 Fix mistake in indexing of url 2018-03-27 17:11:35 -04:00
Jarrod Johnson 66e8ce2dde Merge branch 'master' of github.com:jjohnson42/confluent 2018-03-27 16:35:31 -04:00
Jarrod Johnson 3dd86c71fd Add bmc.hostname to nodeconfig 2018-03-27 16:32:37 -04:00
Jarrod Johnson f97c39cea4 Add hostname to api
The hostname of the BMC is added to the api.
2018-03-27 15:51:14 -04:00
Jarrod Johnson 6671b9aad3 Provide cleaner behavior on timeouts
If a timeout occurred outside of a keeplaive, provide
a more consistent message about the situation.
2018-03-23 08:27:27 -04:00
Jarrod Johnson f88e0bca4c Fix nodeshell hang on incomplete lines
readline would hang because the filehandle was really not ready.
2018-03-19 08:45:13 -04:00
Jarrod Johnson afd366f134 Create invitation management module
This facilitates the generation of invitations and logistics of proving
knowledge of the invitation and the integrity of the certificates.
peercert is to be gotten through getpeercert(binary_form=True) and
local cert through the util function to load from file, since we don't
have another way of getting local certificate.
2018-03-15 19:22:03 -04:00
Jarrod Johnson ad0a7de1e3 Add utility function to get certificate from file
This can be used to get our own certificate, for use in the
multimanager membership establishment.
2018-03-15 18:59:49 -04:00
Jarrod Johnson 026a027603 Fix normalizing unicode in dicts with lists
If there's a list in a list, normalize that as well.
2018-03-15 12:55:32 -04:00
74 changed files with 3413 additions and 6966 deletions
+28 -3
View File
@@ -21,6 +21,7 @@
import optparse
import os
import select
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -47,6 +48,8 @@ argparser.add_option('-c', '--count', action='store_true',
argparser.add_option('-r', '--reverse', action='store_true',
help='Reverse sort order to show biggest output group '
'last')
argparser.add_option('-l', '--log', action='store', type='string', dest='log',
help='Log each output to file, using {node} as a placeholder for node.')
(options, args) = argparser.parse_args()
if sys.stdin.isatty():
argparser.print_help()
@@ -70,17 +73,39 @@ def print_current():
sys.stdout.flush()
fullline = sys.stdin.readline()
printpending = True
clearpending = False
holdoff = 0
while fullline:
for line in fullline.split('\n'):
if not line:
continue
if ': ' not in line:
line = 'UNKNOWN: ' + line
if options.log:
node, output = line.split(': ', 1)
currlog = options.log.format(node=node, nodename=node)
with open(currlog, mode='a') as log:
log.write(output + '\n')
continue
grouped.add_line(*line.split(': ', 1))
if options.watch:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
if not holdoff:
holdoff = os.times()[4] + 0.250
if (holdoff < os.times()[4] or
not select.select((sys.stdin,), (), (), 0.250)[0]):
# print now, nothing pending
holdoff = 0
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
printpending = False
clearpending = True
else:
printpending = True
fullline = sys.stdin.readline()
if not options.watch:
if printpending:
if clearpending:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
+16 -6
View File
@@ -598,9 +598,10 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
if oldtcattr is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
# Request default color scheme, to undo potential weirdness of terminal
sys.stdout.write('\x1b[m')
if sys.stdout.isatty():
sys.stdout.write('\x1b[m')
if fullexit:
if os.environ.get('TERM', '') not in ('linux'):
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(code)
else:
@@ -763,7 +764,10 @@ def conserver_command(filehandle, localcommand):
def get_command_bytes(filehandle, localcommand, cmdlen):
while len(localcommand) < cmdlen:
ready, _, _ = select.select((filehandle,), (), (), 1)
try:
ready, _, _ = select.select((filehandle,), (), (), 1)
except select.error:
ready = ()
if ready:
localcommand += filehandle.read()
return localcommand
@@ -776,7 +780,10 @@ def check_escape_seq(currinput, filehandle):
sys.stdout.flush()
return conserver_command(
filehandle, currinput[len(conserversequence):])
ready, _, _ = select.select((filehandle,), (), (), 3)
try:
ready, _, _ = select.select((filehandle,), (), (), 3)
except select.error:
ready = ()
if not ready: # 3 seconds of no typing
break
currinput += filehandle.read()
@@ -866,8 +873,11 @@ def check_power_state():
while inconsole or not doexit:
if inconsole:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 10)
try:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 10)
except select.error:
rdylist = ()
for fh in rdylist:
if fh == session.connection:
# this only should get called in the
+18 -3
View File
@@ -17,6 +17,7 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -35,7 +36,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes] \
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
\n %prog -c noderange <list of attributes> \
\n %prog -e noderange <attribute names to set> \
\n %prog noderange attribute1=value1 attribute2=value,...
@@ -47,6 +48,8 @@ argparser.add_option('-e', '--environment', action='store_true',
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
@@ -67,12 +70,24 @@ exitcode = 0
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
try:
# setting user output to what the user inputs
if args[1] == 'all':
+3
View File
@@ -35,6 +35,9 @@ argparser = optparse.OptionParser()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-u', '--uefi', dest='uefimode',
action='store_true', default=False,
help='Request UEFI style boot (rather than BIOS)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
+39 -11
View File
@@ -55,6 +55,16 @@ argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
'examine, compare, or restore default')
argparser.add_option('-a', '--advanced', dest='advanced',
action='store_true', default=False,
help='Include advanced settings, which are normally not '
'intended to be used without direction from the '
'relevant server vendor.')
argparser.add_option('-r', '--restoredefault', default=False,
dest='restoredefault', metavar="COMPONENT",
help='Restore the configuration of the node '
'to factory default for given component. '
'Currently only uefi is supported')
(options, args) = argparser.parse_args()
cfgpaths = {
@@ -67,6 +77,8 @@ cfgpaths = {
'bmc.ipv4_gateway': (
'configuration/management_controller/net_interfaces/management',
'ipv4_gateway'),
'bmc.hostname': (
'configuration/management_controller/hostname', 'hostname'),
}
autodeps = {
@@ -163,6 +175,18 @@ for param in args[1:]:
queryparms[path][attrib] = param
session = client.Command()
rcode = 0
if options.restoredefault and options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
elif options.restoredefault:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
@@ -185,12 +209,10 @@ if setmode:
for path in updatebypath:
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
for node in fr['databynode']:
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
if 'error' in r:
sys.stderr.write(node + ': ' + r['error'] + '\n')
if 'errorcode' in r:
rcode |= r['errorcode']
rcode |= client.printerror(r, node)
if 'value' not in r:
continue
keyval = r['value']
@@ -202,12 +224,18 @@ else:
for path in queryparms:
if options.comparedefault:
continue
client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
rc = client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
if rc:
sys.exit(rc)
if printsys or options.exclude:
if printsys == 'all':
printsys = []
path = '/noderange/{0}/configuration/system/all'.format(noderange)
client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode = client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
+1 -1
View File
@@ -47,7 +47,7 @@ session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
+12 -16
View File
@@ -35,18 +35,6 @@ import confluent.screensqueeze as sq
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:
@@ -67,7 +55,8 @@ def printfirm(node, prefix, data):
components = ['all']
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [update [--backup <file>]]|[<components>]")
usage="Usage: "
"%prog <noderange> [list][update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
(options, args) = argparser.parse_args()
@@ -78,9 +67,15 @@ try:
if args[1] == 'update':
upfile = args[2]
else:
if args[1] == 'list':
comps = args[2:]
else:
comps = args[1:]
components = []
for arg in args[1:]:
for arg in comps:
components += arg.split(',')
if not components:
components = ['all']
except IndexError:
argparser.print_help()
@@ -139,16 +134,17 @@ def update_firmware(session, filename):
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
def show_firmware(session):
global exitcode
firmware_shown = False
for component in components:
for res in session.read(
'/noderange/{0}/inventory/firmware/all/{1}'.format(
noderange, component)):
printerror(res)
exitcode |= client.printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
exitcode |= client.printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
+16 -1
View File
@@ -17,6 +17,7 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -47,6 +48,8 @@ argparser.add_option('-e', '--environment', action='store_true',
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear variables')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
@@ -72,7 +75,19 @@ if len(args) > 1:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options, argassign)
try:
# setting user output to what the user inputs
if args[1] == 'all':
+1 -1
View File
@@ -47,7 +47,7 @@ session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
+31 -3
View File
@@ -16,6 +16,7 @@
# limitations under the License.
import codecs
import json
import optparse
import os
import re
@@ -86,12 +87,14 @@ def printerror(res, node=None):
url = '/noderange/{0}/inventory/hardware/all/all'
usedprefixes = set([])
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
argparser.add_option('-j', '--json', action='store_true', help='Output JSON')
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
@@ -114,6 +117,8 @@ if len(args) > 1:
filters.append(re.compile('mac address'))
url = '/noderange/{0}/inventory/hardware/all/all'
try:
if options.json:
databynode = {}
session = client.Command()
for res in session.read(url.format(noderange)):
printerror(res)
@@ -125,9 +130,19 @@ try:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
idx = 2
while (node, prefix) in usedprefixes:
prefix = '{0} {1}'.format(inv['name'], idx)
idx += 1
usedprefixes.add((node, prefix))
if not inv['present']:
if not filters:
print '{0}: {1}: Not Present'.format(node, prefix)
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
else:
print '{0}: {1}: Not Present'.format(node, prefix)
continue
info = inv['information']
info.pop('board_extra', None)
@@ -136,6 +151,11 @@ try:
info.pop('product_extra', None)
if 'memory_type' in info:
if not filters:
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
continue
print_mem_info(node, prefix, info)
continue
for datum in info:
@@ -147,9 +167,17 @@ try:
continue
if info[datum] is None:
continue
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
break
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
pretty(datum),
info[datum]))
if options.json:
print(json.dumps(databynode, sort_keys=True, indent=4,
separators=(',', ': ')))
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(exitcode)
+3 -3
View File
@@ -45,11 +45,11 @@ except IndexError:
sys.exit(1)
client.check_globbing(noderange)
setstate = None
if len(sys.argv) > 2:
if len(args) > 1:
if setstate == 'softoff':
setstate = 'shutdown'
elif not sys.argv[2] in ('stat', 'state', 'status'):
setstate = sys.argv[2]
elif not args[1] in ('stat', 'state', 'status'):
setstate = args[1]
if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
argparser.print_help()
+1 -1
View File
@@ -85,7 +85,7 @@ def run():
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
+1 -1
View File
@@ -86,7 +86,7 @@ def run():
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.screensqueeze as sq
exitcode = 0
def get_update_progress(session, url):
for res in session.read(url):
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
text = '{0}: {1:3.0f}%'.format(status, percent)
return text, status, detail
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
for node in res.get('databynode', ()):
printerror(res['databynode'][node], node)
def download_servicedata(noderange, media):
global exitcode
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/support/servicedata/'.format(noderange)
upargs = {'filename': filename}
noderrs = {}
nodeurls = {}
for res in session.create(resource, upargs):
if 'created' not in res:
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
'error', 'Unknown Error')
continue
watchurl = res['created']
currnode = watchurl.split('/')[1]
nodeurls[currnode] = '/' + watchurl
while nodeurls:
for node in list(nodeurls):
progress, status, err = get_update_progress(
session, nodeurls[node])
if status == 'error':
exitcode = 1
noderrs[node] = err
if status in ('error', 'complete', 'pending'):
list(session.delete(nodeurls[node]))
del nodeurls[node]
output.set_output(node, progress)
time.sleep(2)
allerrnodes = ','.join(noderrs)
if noderrs:
sys.stderr.write(
'Nodes had errors retrieving service data ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
funmap = {
'servicedata': download_servicedata,
}
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> [servicedata] "
"<filename>",
epilog='\nservicedata will save service data to the given '
'directory\n'
'\n\nSee `man %prog` for more info.\n')
(options, args) = argparser.parse_args()
media = None
try:
noderange = args[0]
operation = args[1]
arglength = 2
if operation == 'servicedata':
media = args[2]
arglength = 3
if len(args) > arglength:
argparser.print_help()
sys.exit(1)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
try:
handler = funmap[operation]
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media)
if __name__ == '__main__':
main()
+34 -5
View File
@@ -17,6 +17,7 @@
import anydbm as dbm
import errno
import fnmatch
import hashlib
import os
import shlex
@@ -33,6 +34,21 @@ _attraliases = {
'bmcpass': 'secret.hardwaremanagementpassword',
}
def printerror(res, node=None):
exitcode = 0
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
return exitcode
def cprint(txt):
print(txt)
sys.stdout.flush()
@@ -246,8 +262,7 @@ class Command(object):
certreqs = ssl.CERT_NONE
knownhosts = True
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
cert_reqs=certreqs,
ssl_version=ssl.PROTOCOL_TLSv1)
cert_reqs=certreqs)
if knownhosts:
certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
@@ -297,7 +312,7 @@ def attrrequested(attr, attrlist, seenattributes):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if candidate.lower() == attr.lower():
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
seenattributes.add(truename)
return True
elif attr.lower().startswith(candidate.lower() + '.'):
@@ -323,6 +338,12 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
for attr, val in sorted(
res['databynode'][node].items(),
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
seenattributes.add(attr)
if rename:
printattr = rename.get(attr, attr)
@@ -473,7 +494,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
exitcode = 1
return exitcode
def updateattrib(session, updateargs, nodetype, noderange, options):
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
# update attribute
exitcode = 0
if options.clear:
@@ -506,11 +527,19 @@ def updateattrib(session, updateargs, nodetype, noderange, options):
'attributes/all',
value, key)
sys.exit(exitcode)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
try:
for val in updateargs[1:]:
val = val.split('=')
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
+30 -6
View File
@@ -20,6 +20,11 @@ from datetime import datetime
import json
import struct
try:
unicode
except NameError:
unicode = str
def decodestr(value):
ret = None
try:
@@ -38,17 +43,28 @@ def unicode_dictvalues(dictdata):
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] = decodestr(dictdata[key][i])
elif isinstance(dictdata[key][i], dict):
unicode_dictvalues(dictdata[key][i])
_unicode_list(dictdata[key])
elif isinstance(dictdata[key], dict):
unicode_dictvalues(dictdata[key])
def _unicode_list(currlist):
for i in xrange(len(currlist)):
if isinstance(currlist[i], str):
currlist[i] = decodestr(currlist[i])
elif isinstance(currlist[i], dict):
unicode_dictvalues(currlist[i])
elif isinstance(currlist[i], list):
_unicode_list(currlist[i])
def send(handle, data):
if isinstance(data, str):
if isinstance(data, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, str) or isinstance(data, unicode):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
@@ -74,6 +90,14 @@ def send(handle, data):
handle.sendall(struct.pack("!I", tl))
handle.sendall(sdata)
def recvall(handle, size):
rd = handle.recv(size)
while len(rd) < size:
nd = handle.recv(size - len(rd))
if not nd:
raise Exception("Error reading data")
rd += nd
return rd
def recv(handle):
tl = handle.recv(4)
+216
View File
@@ -37,3 +37,219 @@ alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURR
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
_confluent_get_args()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
CMPARGS+=("")
fi
GENNED=""
for CAND in ${COMP_CANDIDATES[@]}; do
candarray=(${CAND//,/ })
matched=0
for c in "${candarray[@]}"; do
for arg in "${CMPARGS[@]}"; do
if [ "$arg" = "$c" ]; then
matched=1
break
fi
done
done
if [ 0 = $matched ]; then
for c in "${candarray[@]}"; do
GENNED+=" $c"
done
fi
done
}
function _confluent_generic_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeidentify_completion()
{
COMP_CANDIDATES=("on,off -h")
_confluent_generic_completion
}
_confluent_nodesetboot_completion()
{
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
_confluent_generic_completion
}
_confluent_nodepower_completion()
{
COMP_CANDIDATES=("boot,off,on,status -h -p")
_confluent_generic_completion
}
_confluent_nodemedia_completion()
{
COMP_CANDIDATES=("list,upload,attach,detachall -h")
_confluent_get_args
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[-2]} == 'upload' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodefirmware_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeshell_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -c -- ${COMP_WORDS[-1]}))
return
fi
if [ $NUMARGS -gt 3 ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodesupport_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
compopt -o dirnames
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
}
_confluent_nn_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_nr_completion()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
fi
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
#COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_ng_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
complete -F _confluent_nr_completion nodeattrib
complete -F _confluent_nr_completion nodebmcreset
complete -F _confluent_nodesetboot_completion nodeboot
complete -F _confluent_nr_completion nodeconfig
complete -F _confluent_nn_completion nodeconsole
complete -F _confluent_nr_completion nodeeventlog
complete -F _confluent_nodefirmware_completion nodefirmware
complete -F _confluent_ng_completion nodegroupattrib
complete -F _confluent_ng_completion nodegroupremove
complete -F _confluent_nr_completion nodehealth
complete -F _confluent_nodeidentify_completion nodeidentify
complete -F _confluent_nr_completion nodeinventory
complete -F _confluent_nr_completion nodelist
complete -F _confluent_nodemedia_completion nodemedia
complete -F _confluent_nodepower_completion nodepower
complete -F _confluent_nr_completion noderemove
complete -F _confluent_nr_completion nodereseat
complete -F _confluent_nodeshell_completion noderun
complete -F _confluent_nr_completion nodesensors
complete -F _confluent_nodesetboot_completion nodesetboot
complete -F _confluent_nodeshell_completion nodeshell
complete -F _confluent_nodesupport_completion nodesupport
+6 -2
View File
@@ -3,7 +3,7 @@ collate(1) -- Organize text input by node
## SYNOPSIS
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
## DESCRIPTION
@@ -26,6 +26,10 @@ node and group names sorted alphanumerically.
Express all but the most common result group in terms of diff from
the most common result group
* `-l`, `--log`:
Save output per node to individual log files, replacing {node} in the name
with the nodename of each
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
@@ -85,4 +89,4 @@ node and group names sorted alphanumerically.
` Processors.ExecuteDisableBit=Enable`
+10 -3
View File
@@ -3,10 +3,11 @@ nodeattrib(8) -- List or change confluent nodes attributes
## SYNOPSIS
`nodeattrib [-b] <noderange> [<nodeattribute>...]`
`nodeattrib [-b] <noderange> [all|<nodeattribute>...]`
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -19,11 +20,15 @@ displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. Attributes can be
straightforward values, or an expression as documented in nodeattribexpressions(5).
For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null valid.
If `-c` is specified, this will set the nodeattribute to a null value.
This is different from setting the value to an empty string.
If the word all is specified, then all available attributes are given.
Omitting any attribute name or the word 'all' will display only attributes
that are currently set.
For the `groups` attribute, it is possible to add a group by doing
`groups,=<newgroup>`` and to remove by doing `groups^=<oldgroup>`
`groups,=<newgroup>` and to remove by doing `groups^=<oldgroup>`
Note that `nodeattrib <group>` will likely not provide the expected behavior.
See nodegroupattrib(8) command on how to manage attributes on a group level.
@@ -36,6 +41,8 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
Clear specified nodeattributes
* `-e`, `--environment`:
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
* `-p`, `--prompt`:
Request interactive prompting to provide values rather than the command line or environment variables.
## EXAMPLES
* Listing matching nodes of a simple noderange:
+34 -2
View File
@@ -4,12 +4,44 @@ nodeboot(8) -- Reboot a confluent node to a specific device
## SYNOPSIS
`nodeboot <noderange>`
`nodeboot <noderange>` [net|setup]
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
## DESCRIPTION
**nodeboot** reboots nodes in a noderange. If an additional argument is given,
it sets the node to specifically boot to that as the next boot.
it sets the node to specifically boot to that as the next boot. This
performs an immediate reboot without waiting for the OS. To set the boot
device without inducing a reboot, see the `nodesetboot` command.
## OPTIONS
* `-b`, `--bios`:
For a system that supports both BIOS and UEFI style boot, request BIOS style
boot if supported (some platforms will UEFI boot with this flag anyway).
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `default`:
Request a normal default boot with no particular device override
* `cd`:
Request boot from media. Note that this can include physical CD,
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
key devices.
* `network`:
Request boot to network
* `setup`:
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
* `hd`:
Boot straight to hard disk drive
## EXAMPLES
* Booting n3 and n4 to the default boot behavior:
+5
View File
@@ -30,6 +30,11 @@ given as a node expression, as documented in the man page for nodeattribexpressi
Provide detailed data as available. This can include help text and valid
values for a setting.
* `-r`, `--restoredefault`:
Request that the specified component of the targeted nodes will have its
configuration reset to default. Currently the only component implemented
is uefi.
## EXAMPLES
* Showing the current IP configuration of noderange BMC/IMM/XCC:
+1
View File
@@ -1,4 +1,5 @@
nodedefine(8) -- Define new confluent nodes
===================================================================
## SYNOPSIS
+1 -1
View File
@@ -4,7 +4,7 @@ nodefirmware(8) -- Report firmware information on confluent nodes
## SYNOPSIS
`nodefirmware <noderange>`
`nodefirmware <noderange> <components>|core`
`nodefirmware <noderange> list|<components>|core`
`nodefirmware <noderange> update [--backup] <filename>`
## DESCRIPTION
@@ -7,6 +7,7 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
`nodegroupattrib <group> [<nodeattribute>...]`
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -1,4 +1,5 @@
nodegroupdefine(8) -- Define new confluent node group
===================================================================
## SYNOPSIS
@@ -6,7 +7,7 @@ nodegroupdefine(8) -- Define new confluent node group
## DESCRIPTION
`nodegroupdefine` allows the definition of a new node for the confluent management
`nodegroupdefine` allows the definition of a new nodegroup for the confluent management
service. It may only define a single group name at a time.
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
that `nodegroupattrib(8)` will error if a node group does not exist.
+2 -1
View File
@@ -15,7 +15,8 @@ are mounted in an insecure fashion. http is insecure, and https is also
insecure when no meaningful certificate validation is performed. Currently
there is no action that can change this, and this is purely informational. A
future version of software may provide a means to increase security of attached
remote media.
remote media. If no media is mounted, this will provide no output, error
conditions will result in output to standard error.
`detachall` removes all the currently provided media to the host. This unlinks
remote media from urls and deletes uploaded media from the BMC.
+6 -1
View File
@@ -4,7 +4,7 @@ nodepower(8) -- Check or change power state of confluent nodes
## SYNOPSIS
`nodepower <noderange>`
`nodepower <noderange> [on|off|boot|shutdown|reset|status]`
`nodepower <noderange> [-p] [on|off|boot|shutdown|reset|status]`
## DESCRIPTION
@@ -26,6 +26,11 @@ respond.
off will not react to this request.
* `status`: Behave identically to having no argument passed at all.
## OPTIONS
* `-p`, '--showprevious':
Show previous power state for all directives that may change power state.
## EXAMPLES
* Get power state of nodes n1 through n4:
`# nodepower n1-n4`
+4
View File
@@ -48,6 +48,10 @@ themselves, see nodeshell(8).
`n4: 01 10 00`
`n2: 01 10 00`
* If wanting to use literal {} in the command, they must be escaped by doubling:
`# noderun n1-n4 "echo {node} | awk '{{print $1}}'"`
## SEE ALSO
nodeshell(8)
+4 -1
View File
@@ -30,7 +30,10 @@ control.
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `default`:
Request a normal default boot with no particular device override
+4 -1
View File
@@ -27,7 +27,10 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
`n4: hi`
* Setting a new static ip address temporarily on secondary interface of four nodes:
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
* If wanting to use literal {} in the command, they must be escaped by doubling:
`# nodeshell n1-n4 "ps | awk '{{print $1}}'"`
## SEE ALSO
+25
View File
@@ -0,0 +1,25 @@
nodesupport(8) -- Utilities for interacting with vendor support
=================================================================
## SYNOPSIS
`nodesupport <noderange> servicedata <directory or filename>`
## DESCRIPTION
`nodesupport` provides capabilities associated with interactiong with support.
Currently it only has the `servicedata` subcommand. `servicedata` takes
an argument that is either a directory name (that can be used for a single node
or multiple nodes) or a file name (only to be used with single node noderange)
## EXAMPLES
* Download support data from a single node to a specific filename
`# nodesupport d1 servicedata svcdata.out`
`d1:initializing: 15%`
* Download support data from multiple nodes to a directory
`# nodesupport d1-d4 servicedata service/`
`d1:initializing: 0% d2:initializing: 0% d3:initializing: 0% d4:initializing: 0%`
`# ls service/`
`d1.svcdata d2.svcdata d3.svcdata d4.svcdata`
@@ -0,0 +1,35 @@
#!/usr/bin/env python
# This is a sample python script for going through all observed mac addresses
# and assuming they are BMC related and printing nodeattrib commands
# for each node to access the bmc using the interface specified on the command
# line
# Not necessarily as useful if there may be mistakes in the
# net.switch/net.switchport attributes, but a handy utility in a pinch when
# you really know
import confluent.client as cl
import socket
import struct
c = cl.Command()
macs = []
interface = sys.argv[1]
for mac in c.read('/networking/macs/by-mac/'):
macs.append(mac['item']['href'])
for mac in macs:
macinfo = list(c.read('/networking/macs/by-mac/{0}'.format(mac)))[0]
if 'possiblenode' in macinfo and macinfo['possiblenode']:
if macinfo['macsonport'] > 1:
print('#Ambiguous set of macs on port for ' + macinfo[
'possiblenode'])
prefix = int(mac.replace('-', '')[:6], 16) ^ 0b100000000000000000
prefix = prefix << 8
prefix |= 0xff
suffix = int(mac.replace('-', '')[6:], 16)
suffix |= 0xfe000000
rawn = struct.pack('!QLL', 0xfe80000000000000, prefix, suffix)
bmc = socket.inet_ntop(socket.AF_INET6, rawn)
print('nodeattrib {0} bmc={1}%{2}'.format(macinfo['possiblenode'],
bmc, interface))
-17
View File
@@ -1,17 +0,0 @@
[metadata]
name = confluent_common
summary = Confluent Common Libraries
description-file =
README.txt
author = Jarrod Johnson
author-email = jjohnson2@lenovo.com
home-page = http://xcat.sf.net/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
[files]
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python
import argparse
import errno
import os
import socket
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'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.client as client
import confluent.tlvdata as tlvdata
try:
input = raw_input
except NameError:
pass
def make_certificate():
umask = os.umask(0077)
try:
os.makedirs('/etc/confluent/cfg')
except OSError as e:
if e.errno == errno.EEXIST and os.path.isdir('/etc/confluent/cfg'):
pass
else:
raise
if subprocess.check_call(
'openssl ecparam -name secp384r1 -genkey -out '
'/etc/confluent/privkey.pem'.split(' ')):
raise Exception('Error generating private key')
if subprocess.check_call('openssl req -new -x509 -key '
'/etc/confluent/privkey.pem -days 7300 -out '
'/etc/confluent/srvcert.pem -subj /CN='
'{0}'.format(socket.gethostname()).split(' ')):
raise Exception('Error generating certificate')
print('Certificate generated successfully')
os.umask(umask)
def show_invitation(name):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'invite', 'name': name}})
invite = tlvdata.recv(s)['collective']
if 'error' in invite:
sys.stderr.write(invite['error'] + '\n')
return
print('{0}'.format(invite['invitation']))
def join_collective(server, invitation):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
while not invitation:
invitation = raw_input('Paste the invitation here: ')
tlvdata.send(s, {'collective': {'operation': 'join',
'invitation': invitation,
'server': server}})
res = tlvdata.recv(s)
res = res.get('collective',
{'status': 'Unknown response: ' + repr(res)})
print(res.get('status', res.get('error', repr(res))))
def show_collective():
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'show'}})
res = tlvdata.recv(s)
if 'error' in res['collective']:
print(res['collective']['error'])
return
if 'quorum' in res['collective']:
print('Quorum: {0}'.format(res['collective']['quorum']))
print('Leader: {0}'.format(res['collective']['leader']))
if 'active' in res['collective']:
if res['collective']['active']:
print('Active collective members:')
for member in res['collective']['active']:
print(' {0}'.format(member))
if res['collective']['offline']:
print('Offline collective members:')
for member in res['collective']['offline']:
print(' {0}'.format(member))
else:
print('Run collective show on leader for more data')
def main():
a = argparse.ArgumentParser(description='Confluent server utility')
sp = a.add_subparsers(dest='command')
gc = sp.add_parser('gencert', help='Generate Confluent Certificates for '
'collective mode and remote CLI access')
sl = sp.add_parser('show', help='Show information about the collective')
ic = sp.add_parser('invite', help='Generate a invitation to allow a new '
'confluent instance to join as a '
'collective member')
ic.add_argument('name', help='Name of server to invite to join the '
'collective')
jc = sp.add_parser('join', help='Join a collective')
jc.add_argument('server', help='A server currently in the collective')
jc.add_argument('-i', help='Invitation provided by runniing invite on an '
'existing collective member')
cmdset = a.parse_args()
if cmdset.command == 'gencert':
make_certificate()
elif cmdset.command == 'invite':
show_invitation(cmdset.name)
elif cmdset.command == 'join':
join_collective(cmdset.server, cmdset.i)
elif cmdset.command == 'show':
show_collective()
if __name__ == '__main__':
main()
+30 -4
View File
@@ -16,6 +16,7 @@
# limitations under the License.
import getpass
import optparse
import sys
import os
@@ -32,6 +33,8 @@ argparser = optparse.OptionParser(
usage="Usage: %prog [options] [dump|restore] [path]")
argparser.add_option('-p', '--password',
help='Password to use to protect/unlock a protected dump')
argparser.add_option('-i', '--interactivepassword', help='Prompt for password',
action='store_true')
argparser.add_option('-r', '--redact', action='store_true',
help='Redact potentially sensitive data rather than store')
argparser.add_option('-u', '--unprotected', action='store_true',
@@ -39,6 +42,14 @@ argparser.add_option('-u', '--unprotected', action='store_true',
' the key information. Fields will be encrypted, '
'but keys.json will contain unencrypted decryption'
' keys that may be used to read the dump')
argparser.add_option('-s', '--skipkeys', action='store_true',
help='This specifies to dump the encrypted data without '
'dumping the keys needed to decrypt it. This is '
'suitable for an automated incremental backup, '
'where an earlier password protected dump has a '
'protected keys.json file, and only the protected '
'data is needed. keys do not change and as such '
'they do not require incremental backup')
(options, args) = argparser.parse_args()
if len(args) != 2 or args[0] not in ('dump', 'restore'):
argparser.print_help()
@@ -51,17 +62,32 @@ if args[0] == 'restore':
if pid is not None:
print("Confluent is running, must shut down to restore db")
sys.exit(1)
cfm.restore_db_from_directory(dumpdir, options.password)
try:
cfm.restore_db_from_directory(dumpdir, options.password)
except Exception as e:
print(str(e))
sys.exit(1)
elif args[0] == 'dump':
if options.password is None and not (options.unprotected or options.redact):
password = options.password
if not password and options.interactivepassword:
passcfm = None
while passcfm is None or password != passcfm:
password = getpass.getpass(
'Enter password to protect the backup: ')
passcfm = getpass.getpass('Confirm password to protect the backup: ')
if password is None and not (options.unprotected or options.redact
or options.skipkeys):
print("Must indicate a password to protect or -u to opt opt of "
"secure value protection or -r to skip all protected data")
"secure value protection or -r to redact sensitive information, "
"or -s to do encrypted backup that requires keys.json from "
"another backup to restore.")
sys.exit(1)
os.umask(077)
main._initsecurity(conf.get_config())
if not os.path.exists(dumpdir):
os.makedirs(dumpdir)
cfm.dump_db_to_directory(dumpdir, options.password, options.redact)
cfm.dump_db_to_directory(dumpdir, options.password, options.redact,
options.skipkeys)
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
umask 0077
openssl ecparam -name secp384r1 -genkey -out /etc/confluent/privkey.pem
openssl req -new -x509 -key /etc/confluent/privkey.pem -days 760 -out /etc/confluent/srvcert.pem -subj /CN=$(hostname)
-1
View File
@@ -1 +0,0 @@
__version__ = "1.5.0.dev395.ggacb3a20"
+1 -1
View File
@@ -22,7 +22,7 @@
import confluent.config.configmanager as configmanager
import eventlet
import eventlet.tpool
import Crypto.Protocol.KDF as KDF
import Cryptodome.Protocol.KDF as KDF
import hashlib
import hmac
import multiprocessing
@@ -0,0 +1,60 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This handles the process of generating and tracking/validating invites
import base64
import hashlib
import hmac
import os
pending_invites = {}
def create_server_invitation(servername):
servername = servername.encode('utf-8')
randbytes = (3 - ((len(servername) + 2) % 3)) % 3 + 64
invitation = os.urandom(randbytes)
pending_invites[servername] = invitation
return base64.b64encode(servername + b'@' + invitation)
def create_client_proof(invitation, mycert, peercert):
return hmac.new(invitation, peercert + mycert, hashlib.sha256).digest()
def check_server_proof(invitation, mycert, peercert, proof):
validproof = hmac.new(invitation, mycert + peercert, hashlib.sha256
).digest()
return proof == validproof
def check_client_proof(servername, mycert, peercert, proof):
servername = servername.encode('utf-8')
if servername not in pending_invites:
return False
invitation = pending_invites[servername]
validproof = hmac.new(invitation, mycert + peercert, hashlib.sha256
).digest()
if proof == validproof:
# We know that the client knew the secret, and that it measured our
# certificate, and thus calling code can bless the certificate, and
# we can forget the invitation
del pending_invites[servername]
# We now want to prove to the client that we also know the secret,
# and that we measured their certificate well
# Now to generate an answer...., reverse the cert order so our answer
# is different, but still proving things
return hmac.new(invitation, peercert + mycert, hashlib.sha256
).digest()
# The given proof did not verify the invitation
return False
@@ -0,0 +1,590 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import confluent.collective.invites as invites
import confluent.config.configmanager as cfm
import confluent.exceptions as exc
import confluent.log as log
import confluent.tlvdata as tlvdata
import confluent.util as util
import eventlet
import eventlet.green.socket as socket
import eventlet.green.ssl as ssl
import eventlet.green.threading as threading
import greenlet
import random
try:
import OpenSSL.crypto as crypto
except ImportError:
# while not always required, we use pyopenssl required for at least
# collective
crypto = None
currentleader = None
follower = None
retrythread = None
class ContextBool(object):
def __init__(self):
self.active = False
self.mylock = threading.RLock()
def __enter__(self):
self.active = True
self.mylock.__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
self.active = False
self.mylock.__exit__(exc_type, exc_val, exc_tb)
connecting = ContextBool()
leader_init = ContextBool()
def connect_to_leader(cert=None, name=None, leader=None):
global currentleader
global follower
if leader is None:
leader = currentleader
log.log({'info': 'Attempting connection to leader {0}'.format(leader),
'subsystem': 'collective'})
try:
remote = connect_to_collective(cert, leader)
except socket.error as e:
log.log({'error': 'Collective connection attempt to {0} failed: {1}'
''.format(leader, str(e)),
'subsystem': 'collective'})
return False
with connecting:
with cfm._initlock:
tlvdata.recv(remote) # the banner
tlvdata.recv(remote) # authpassed... 0..
if name is None:
name = get_myname()
tlvdata.send(remote, {'collective': {'operation': 'connect',
'name': name,
'txcount': cfm._txcount}})
keydata = tlvdata.recv(remote)
if not keydata:
return False
if 'error' in keydata:
if 'backoff' in keydata:
log.log({
'info': 'Collective initialization in progress on '
'{0}'.format(leader),
'subsystem': 'collective'})
return False
if 'leader' in keydata:
log.log(
{'info': 'Prospective leader {0} has redirected this '
'member to {1}'.format(leader, keydata['leader']),
'subsystem': 'collective'})
ldrc = cfm.get_collective_member_by_address(
keydata['leader'])
if ldrc and ldrc['name'] == name:
raise Exception("Redirected to self")
return connect_to_leader(name=name,
leader=keydata['leader'])
if 'txcount' in keydata:
log.log({'info':
'Prospective leader {0} has inferior '
'transaction count, becoming leader'
''.format(leader), 'subsystem': 'collective',
'subsystem': 'collective'})
return become_leader(remote)
return False
follower.kill()
cfm.stop_following()
follower = None
if follower:
follower.kill()
cfm.stop_following()
follower = None
log.log({'info': 'Following leader {0}'.format(leader),
'subsystem': 'collective'})
colldata = tlvdata.recv(remote)
globaldata = tlvdata.recv(remote)
dbi = tlvdata.recv(remote)
dbsize = dbi['dbsize']
dbjson = ''
while (len(dbjson) < dbsize):
ndata = remote.recv(dbsize - len(dbjson))
if not ndata:
try:
remote.close()
except Exception:
pass
raise Exception("Error doing initial DB transfer")
dbjson += ndata
cfm.clear_configuration()
try:
cfm._restore_keys(keydata, None, sync=False)
for c in colldata:
cfm._true_add_collective_member(c, colldata[c]['address'],
colldata[c]['fingerprint'],
sync=False)
for globvar in globaldata:
cfm.set_global(globvar, globaldata[globvar], False)
cfm._txcount = dbi.get('txcount', 0)
cfm.ConfigManager(tenant=None)._load_from_json(dbjson,
sync=False)
cfm.commit_clear()
except Exception:
cfm.stop_following()
cfm.rollback_clear()
raise
currentleader = leader
#spawn this as a thread...
follower = eventlet.spawn(follow_leader, remote)
return True
def follow_leader(remote):
global currentleader
cleanexit = False
try:
cfm.follow_channel(remote)
except greenlet.GreenletExit:
cleanexit = True
finally:
if cleanexit:
log.log({'info': 'Previous following cleanly closed',
'subsystem': 'collective'})
return
log.log({'info': 'Current leader has disappeared, restarting '
'collective membership', 'subsystem': 'collective'})
# The leader has folded, time to startup again...
cfm.stop_following()
currentleader = None
eventlet.spawn_n(start_collective)
def connect_to_collective(cert, member):
remote = socket.create_connection((member, 13001))
# TLS cert validation is custom and will not pass normal CA vetting
# to override completely in the right place requires enormous effort, so just defer until after connect
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE, keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
if cert:
fprint = cert
else:
collent = cfm.get_collective_member_by_address(member)
fprint = collent['fingerprint']
if not util.cert_matches(fprint, remote.getpeercert(binary_form=True)):
# probably Janeway up to something
raise Exception("Certificate mismatch in the collective")
return remote
def get_myname():
try:
with open('/etc/confluent/cfg/myname', 'r') as f:
return f.read().strip()
except IOError:
myname = socket.gethostname()
with open('/etc/confluent/cfg/myname', 'w') as f:
f.write(myname)
return myname
def handle_connection(connection, cert, request, local=False):
global currentleader
global retrythread
operation = request['operation']
if cert:
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
else:
if not local:
return
if 'show' == operation:
if not list(cfm.list_collective()):
tlvdata.send(connection,
{'collective': {'error': 'Collective mode not '
'enabled on this '
'system'}})
return
if follower:
linfo = cfm.get_collective_member_by_address(currentleader)
remote = socket.create_connection((currentleader, 13001))
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
cert = remote.getpeercert(binary_form=True)
if not (linfo and util.cert_matches(
linfo['fingerprint'],
cert)):
remote.close()
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
tlvdata.recv(remote) # ignore banner
tlvdata.recv(remote) # ignore authpassed: 0
tlvdata.send(remote,
{'collective': {'operation': 'getinfo',
'name': get_myname()}})
collinfo = tlvdata.recv(remote)
else:
collinfo = {}
populate_collinfo(collinfo)
try:
cfm.check_quorum()
collinfo['quorum'] = True
except exc.DegradedCollective:
collinfo['quorum'] = False
tlvdata.send(connection, {'collective': collinfo})
return
if 'invite' == operation:
try:
cfm.check_quorum()
except exc.DegradedCollective:
tlvdata.send(connection,
{'collective':
{'error': 'Collective does not have quorum'}})
return
#TODO(jjohnson2): Cannot do the invitation if not the head node, the certificate hand-carrying
#can't work in such a case.
name = request['name']
invitation = invites.create_server_invitation(name)
tlvdata.send(connection,
{'collective': {'invitation': invitation}})
connection.close()
if 'join' == operation:
invitation = request['invitation']
try:
invitation = base64.b64decode(invitation)
name, invitation = invitation.split('@', 1)
except Exception:
tlvdata.send(
connection,
{'collective':
{'status': 'Invalid token format'}})
connection.close()
return
host = request['server']
try:
remote = socket.create_connection((host, 13001))
# This isn't what it looks like. We do CERT_NONE to disable
# openssl verification, but then use the invitation as a
# shared secret to validate the certs as part of the join
# operation
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
except Exception:
tlvdata.send(
connection,
{'collective':
{'status': 'Failed to connect to {0}'.format(host)}})
connection.close()
return
mycert = util.get_certificate_from_file(
'/etc/confluent/srvcert.pem')
cert = remote.getpeercert(binary_form=True)
proof = base64.b64encode(invites.create_client_proof(
invitation, mycert, cert))
tlvdata.recv(remote) # ignore banner
tlvdata.recv(remote) # ignore authpassed: 0
tlvdata.send(remote, {'collective': {'operation': 'enroll',
'name': name, 'hmac': proof}})
rsp = tlvdata.recv(remote)
if 'error' in rsp:
tlvdata.send(connection, {'collective':
{'status': rsp['error']}})
connection.close()
return
proof = rsp['collective']['approval']
proof = base64.b64decode(proof)
j = invites.check_server_proof(invitation, mycert, cert, proof)
if not j:
remote.close()
tlvdata.send(connection, {'collective':
{'status': 'Bad server token'}})
connection.close()
return
tlvdata.send(connection, {'collective': {'status': 'Success'}})
connection.close()
currentleader = rsp['collective']['leader']
f = open('/etc/confluent/cfg/myname', 'w')
f.write(name)
f.close()
log.log({'info': 'Connecting to collective due to join',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, rsp['collective'][
'fingerprint'], name)
if 'enroll' == operation:
#TODO(jjohnson2): error appropriately when asked to enroll, but the master is elsewhere
mycert = util.get_certificate_from_file('/etc/confluent/srvcert.pem')
proof = base64.b64decode(request['hmac'])
myrsp = invites.check_client_proof(request['name'], mycert,
cert, proof)
if not myrsp:
tlvdata.send(connection, {'error': 'Invalid token'})
connection.close()
return
myrsp = base64.b64encode(myrsp)
fprint = util.get_fingerprint(cert)
myfprint = util.get_fingerprint(mycert)
cfm.add_collective_member(get_myname(),
connection.getsockname()[0], myfprint)
cfm.add_collective_member(request['name'],
connection.getpeername()[0], fprint)
myleader = get_leader(connection)
ldrfprint = cfm.get_collective_member_by_address(
myleader)['fingerprint']
tlvdata.send(connection,
{'collective': {'approval': myrsp,
'fingerprint': ldrfprint,
'leader': get_leader(connection)}})
if 'assimilate' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not droneinfo:
tlvdata.send(connection,
{'error': 'Unrecognized leader, '
'redo invitation process'})
return
if not util.cert_matches(droneinfo['fingerprint'], cert):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
return
if request['txcount'] < cfm._txcount:
tlvdata.send(connection,
{'error': 'Refusing to be assimilated by inferior'
'transaction count',
'txcount': cfm._txcount,})
return
if connecting.active:
# don't try to connect while actively already trying to connect
tlvdata.send(connection, {'status': 0})
connection.close()
return
if (currentleader == connection.getpeername()[0] and
follower and follower.isAlive()):
# if we are happily following this leader already, don't stir
# the pot
tlvdata.send(connection, {'status': 0})
connection.close()
return
log.log({'info': 'Connecting in response to assimilation',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, None, None,
leader=connection.getpeername()[0])
tlvdata.send(connection, {'status': 0})
connection.close()
if 'getinfo' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
cert)):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
collinfo = {}
populate_collinfo(collinfo)
tlvdata.send(connection, collinfo)
if 'connect' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
cert)):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
myself = connection.getsockname()[0]
if connecting.active:
tlvdata.send(connection, {'error': 'Connecting right now',
'backoff': True})
connection.close()
return
if myself != get_leader(connection):
tlvdata.send(
connection,
{'error': 'Cannot assimilate, our leader is '
'in another castle', 'leader': currentleader})
connection.close()
return
if request['txcount'] > cfm._txcount:
retire_as_leader()
tlvdata.send(connection,
{'error': 'Client has higher tranasaction count, '
'should assimilate me, connecting..',
'txcount': cfm._txcount})
log.log({'info': 'Connecting to leader due to superior '
'transaction count', 'subsystem': collective})
eventlet.spawn_n(connect_to_leader, None, None,
connection.getpeername()[0])
connection.close()
return
if retrythread:
retrythread.cancel()
retrythread = None
with leader_init:
cfm.update_collective_address(request['name'],
connection.getpeername()[0])
tlvdata.send(connection, cfm._dump_keys(None, False))
tlvdata.send(connection, cfm._cfgstore['collective'])
tlvdata.send(connection, cfm.get_globals())
cfgdata = cfm.ConfigManager(None)._dump_to_json()
tlvdata.send(connection, {'txcount': cfm._txcount,
'dbsize': len(cfgdata)})
connection.sendall(cfgdata)
#tlvdata.send(connection, {'tenants': 0}) # skip the tenants for now,
# so far unused anyway
if not cfm.relay_slaved_requests(drone, connection):
if not retrythread: # start a recovery if everyone else seems
# to have disappeared
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
# ok, we have a connecting member whose certificate checks out
# He needs to bootstrap his configuration and subscribe it to updates
def populate_collinfo(collinfo):
iam = get_myname()
collinfo['leader'] = iam
collinfo['active'] = list(cfm.cfgstreams)
activemembers = set(cfm.cfgstreams)
activemembers.add(iam)
collinfo['offline'] = []
for member in cfm.list_collective():
if member not in activemembers:
collinfo['offline'].append(member)
def try_assimilate(drone):
try:
remote = connect_to_collective(None, drone)
except socket.error:
# Oh well, unable to connect, hopefully the rest will be
# in order
return
tlvdata.send(remote, {'collective': {'operation': 'assimilate',
'name': get_myname(),
'txcount': cfm._txcount}})
tlvdata.recv(remote) # the banner
tlvdata.recv(remote) # authpassed... 0..
answer = tlvdata.recv(remote)
if not answer:
log.log(
{'error':
'No answer from {0} while trying to assimilate'.format(
drone),
'subsystem': 'collective'})
return
if 'txcount' in answer:
log.log({'info': 'Deferring to {0} due to transaction count'.format(
drone), 'subsystem': 'collective'})
connect_to_leader(None, None, leader=remote.getpeername()[0])
return
if 'error' in answer:
log.log({
'error': 'Error encountered while attempting to '
'assimilate {0}: {1}'.format(drone, answer['error']),
'subsystem': 'collective'})
return
log.log({'info': 'Assimilated {0} into collective'.format(drone),
'subsystem': 'collective'})
def get_leader(connection):
if currentleader is None or connection.getpeername()[0] == currentleader:
if currentleader is None:
msg = 'Becoming leader as no leader known'
else:
msg = 'Becoming leader because {0} attempted to connect and it ' \
'is current leader'.format(currentleader)
log.log({'info': msg, 'subsystem': 'collective'})
become_leader(connection)
return currentleader
def retire_as_leader():
global currentleader
cfm.stop_leading()
currentleader = None
def become_leader(connection):
global currentleader
global follower
global retrythread
log.log({'info': 'Becoming leader of collective',
'subsystem': 'collective'})
if follower:
follower.kill()
cfm.stop_following()
follower = None
if retrythread:
retrythread.cancel()
retrythread = None
currentleader = connection.getsockname()[0]
skipaddr = connection.getpeername()[0]
myname = get_myname()
skipem = set(cfm.cfgstreams)
skipem.add(currentleader)
skipem.add(skipaddr)
for member in cfm.list_collective():
dronecandidate = cfm.get_collective_member(member)['address']
if dronecandidate in skipem or member == myname:
continue
eventlet.spawn_n(try_assimilate, dronecandidate)
def startup():
members = list(cfm.list_collective())
if len(members) < 2:
# Not in collective mode, return
return
eventlet.spawn_n(start_collective)
def start_collective():
global follower
global retrythread
if follower:
follower.kill()
cfm.stop_following()
follower = None
try:
if cfm.cfgstreams:
cfm.check_quorum()
# Do not start if we have quorum and are leader
return
except exc.DegradedCollective:
pass
if leader_init.active: # do not start trying to connect if we are
# xmitting data to a follower
return
myname = get_myname()
for member in sorted(list(cfm.list_collective())):
if member == myname:
continue
if cfm.cfgleader is None:
cfm.stop_following(True)
ldrcandidate = cfm.get_collective_member(member)['address']
log.log({'info': 'Performing startup attempt to {0}'.format(
ldrcandidate), 'subsystem': 'collective'})
if connect_to_leader(name=myname, leader=ldrcandidate):
break
else:
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
@@ -148,6 +148,20 @@ node = {
# 'autonode.servername, so that would not need to be '
# 'copied ')
# },
'collective.manager': {
'description': ('When in collective mode, the member of the '
'collective currently considered to be responsible '
'for this node. At a future date, this may be '
'modified automatically if another attribute '
'indicates candidate managers, either for '
'high availability or load balancing purposes.')
},
'discovery.passwordrules': {
'description': 'Any specified rules shall be configured on the BMC '
'upon discovery. "expiration=no,loginfailures=no" '
'would disable password expiration and login failures '
'triggering a lockout.'
},
'discovery.policy': {
'description': 'Policy to use for auto-configuration of discovered '
'and identified nodes. Valid values are "manual", '
File diff suppressed because it is too large Load Diff
+178 -14
View File
@@ -23,14 +23,18 @@
# there should be no more than one handler per node
import codecs
import collections
import confluent.collective.manager as collective
import confluent.config.configmanager as configmodule
import confluent.exceptions as exc
import confluent.interface.console as conapi
import confluent.log as log
import confluent.core as plugin
import confluent.tlvdata as tlvdata
import confluent.util as util
import eventlet
import eventlet.event
import eventlet.green.socket as socket
import eventlet.green.ssl as ssl
import pyte
import random
import time
@@ -138,11 +142,13 @@ def pytechars2line(chars, maxlen=None):
class ConsoleHandler(object):
_plugin_path = '/nodes/{0}/_console/session'
_logtobuffer = True
_genwatchattribs = frozenset(('console.method', 'console.logging'))
_genwatchattribs = frozenset(('console.method', 'console.logging',
'collective.manager'))
def __init__(self, node, configmanager):
self.clearpending = False
self._dologging = True
self._is_local = True
self._isondemand = False
self.error = None
self._retrytime = 0
@@ -214,7 +220,7 @@ class ConsoleHandler(object):
def check_isondemand(self):
self._dologging = True
attrvalue = self.cfgmgr.get_node_attributes(
(self.node,), ('console.logging',))
(self.node,), ('console.logging', 'collective.manager'))
if self.node not in attrvalue:
self._isondemand = False
elif 'console.logging' not in attrvalue[self.node]:
@@ -225,6 +231,23 @@ class ConsoleHandler(object):
self._isondemand = True
if (attrvalue[self.node]['console.logging']['value']) in ('none', 'memory'):
self._dologging = False
self.check_collective(attrvalue)
def check_collective(self, attrvalue):
myc = attrvalue.get(self.node, {}).get('collective.manager', {}).get(
'value', None)
if configmodule.list_collective() and not myc:
self._is_local = False
self._detach()
self._disconnect()
if myc and myc != collective.get_myname():
# Do not do console connect for nodes managed by another
# confluent collective member
self._is_local = False
self._detach()
self._disconnect()
else:
self._is_local = True
def get_buffer_age(self):
"""Return age of buffered data
@@ -236,6 +259,10 @@ class ConsoleHandler(object):
return False
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
if 'collective.manager' in nodeattribs[self.node]:
attrval = configmanager.get_node_attributes(self.node,
'collective.manager')
self.check_collective(attrval)
if 'console.logging' in nodeattribs[self.node]:
# decide whether logging changes how we react or not
self._dologging = True
@@ -284,6 +311,10 @@ class ConsoleHandler(object):
'none or interactive,\r\nconnection loss, or service restart]')
self.clearpending = True
def _detach(self):
for ses in list(self.livesessions):
ses.detach()
def _disconnect(self):
if self.connectionthread:
self.connectionthread.kill()
@@ -305,6 +336,8 @@ class ConsoleHandler(object):
self._disconnect()
def _connect(self):
if not self._is_local:
return
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread = None
@@ -320,14 +353,17 @@ class ConsoleHandler(object):
self.reconnect.cancel()
self.reconnect = None
try:
self._console = plugin.handle_path(
self._console = list(plugin.handle_path(
self._plugin_path.format(self.node),
"create", self.cfgmgr)
"create", self.cfgmgr))[0]
except (exc.NotImplementedException, exc.NotFoundException):
self._console = None
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
if _tracelog:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
else:
print(traceback.format_exc())
if not isinstance(self._console, conapi.Console):
self.clearbuffer()
self.connectstate = 'unconnected'
@@ -413,6 +449,9 @@ class ConsoleHandler(object):
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread = None
if self._attribwatcher:
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
def get_console_output(self, data):
# Spawn as a greenthread, return control as soon as possible
@@ -482,9 +521,6 @@ class ConsoleHandler(object):
eventdata |= 1
if self.shiftin is not None:
eventdata |= 2
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
# TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
@@ -493,6 +529,10 @@ class ConsoleHandler(object):
self.feedbuffer(b'\x1bc')
self._send_rcpts(b'\x1bc')
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
def _send_rcpts(self, data):
for rcpt in list(self.livesessions):
@@ -568,7 +608,12 @@ def _nodechange(added, deleting, configmanager):
def _start_tenant_sessions(cfm):
for node in cfm.list_nodes():
nodeattrs = cfm.get_node_attributes(cfm.list_nodes(), 'collective.manager')
for node in nodeattrs:
manager = nodeattrs[node].get('collective.manager', {}).get('value',
None)
if manager and collective.get_myname() != manager:
continue
try:
connect_node(node, cfm)
except:
@@ -583,12 +628,118 @@ def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager, username=None):
def connect_node(node, configmanager, username=None, direct=True):
attrval = configmanager.get_node_attributes(node, 'collective.manager')
myc = attrval.get(node, {}).get('collective.manager', {}).get(
'value', None)
myname = collective.get_myname()
if myc and myc != collective.get_myname() and direct:
minfo = configmodule.get_collective_member(myc)
return ProxyConsole(node, minfo, myname, configmanager, username)
consk = (node, configmanager.tenant)
if consk not in _handled_consoles:
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
return _handled_consoles[consk]
# A stub console handler that just passes through to a remote confluent
# collective member. It can skip the multi-session sharing as that is handled
# remotely
class ProxyConsole(object):
_genwatchattribs = frozenset(('collective.manager',))
def __init__(self, node, managerinfo, myname, configmanager, user):
self.skipreplay = True
self.managerinfo = managerinfo
self.myname = myname
self.cfm = configmanager
self.node = node
self.user = user
self.remote = None
self.clisession = None
self._attribwatcher = configmanager.watch_attributes(
(self.node,), self._genwatchattribs, self._attribschanged)
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
if self.clisession:
self.clisession.detach()
self.clisession = None
def relay_data(self):
data = tlvdata.recv(self.remote)
while data:
self.data_handler(data)
data = tlvdata.recv(self.remote)
def get_buffer_age(self):
# the server sends a buffer age if appropriate, no need to handle
# it explicitly in the proxy instance
return False
def get_recent(self):
# Again, delegate this to the remote collective member
self.skipreplay = False
return b''
def write(self, data):
# Relay data to the collective manager
try:
tlvdata.send(self.remote, data)
except Exception:
if self.clisession:
self.clisession.detach()
self.clisession = None
def attachsession(self, session):
self.clisession = session
self.data_handler = session.data_handler
termreq = {
'proxyconsole': {
'name': self.myname,
'user': self.user,
'tenant': self.cfm.tenant,
'node': self.node,
'skipreplay': self.skipreplay,
#TODO(jjohnson2): declare myself as a proxy,
#facilitate redirect rather than relay on manager change
},
}
try:
remote = socket.create_connection((self.managerinfo['address'], 13001))
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
if not util.cert_matches(self.managerinfo['fingerprint'],
remote.getpeercert(binary_form=True)):
raise Exception('Invalid peer certificate')
except Exception:
eventlet.sleep(3)
if self.clisession:
self.clisession.detach()
self.detachsession(None)
return
tlvdata.recv(remote)
tlvdata.recv(remote)
tlvdata.send(remote, termreq)
self.remote = remote
eventlet.spawn(self.relay_data)
def detachsession(self, session):
# we will disappear, so just let that happen...
if self.remote:
try:
tlvdata.send(self.remote, {'operation': 'stop'})
except Exception:
pass
self.clisession = None
def send_break(self):
tlvdata.send(self.remote, {'operation': 'break'})
def reopen(self):
tlvdata.send(self.remote, {'operation': 'reopen'})
# 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,
@@ -610,10 +761,9 @@ class ConsoleSession(object):
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
connector = connect_node
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False):
skipreplay=False, direct=True):
self.registered = False
self.tenant = configmanager.tenant
if not configmanager.is_node(node):
@@ -621,6 +771,8 @@ class ConsoleSession(object):
self.username = username
self.node = node
self.configmanager = configmanager
self.direct = direct # true if client is directly connected versus
# relay
self.connect_session()
self.registered = True
self._evt = None
@@ -649,7 +801,7 @@ class ConsoleSession(object):
between console and shell.
"""
self.conshdl = connect_node(self.node, self.configmanager,
self.username)
self.username, self.direct)
def send_break(self):
"""Send break to remote system
"""
@@ -680,6 +832,18 @@ class ConsoleSession(object):
self._evt = None
self.reghdl = None
def detach(self):
"""Handler for the console handler to detach so it can reattach,
currently to facilitate changing from one collective.manager to
another
:return:
"""
self.conshdl.detachsession(self)
self.connect_session()
self.conshdl.attachsession(self)
self.write = self.conshdl.write
def got_data(self, data):
"""Receive data from console and buffer
+257 -15
View File
@@ -35,7 +35,10 @@
import confluent
import confluent.alerts as alerts
import confluent.tlvdata as tlvdata
import confluent.config.attributes as attrscheme
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
import confluent.discovery.core as disco
import confluent.interface.console as console
import confluent.exceptions as exc
@@ -46,11 +49,27 @@ try:
import confluent.shellmodule as shellmodule
except ImportError:
pass
try:
import OpenSSL.crypto as crypto
except ImportError:
# Only required for collective mode
crypto = None
import confluent.util as util
import eventlet.greenpool as greenpool
import eventlet.green.ssl as ssl
import eventlet.queue as queue
import itertools
import os
try:
import cPickle as pickle
except ImportError:
import pickle
import socket
import struct
import sys
pluginmap = {}
dispatch_plugins = (b'ipmi', u'ipmi')
def seek_element(currplace, currkey):
@@ -154,6 +173,10 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'hostname': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'identifier': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -178,6 +201,14 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'advanced': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'clear': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
})
},
},
'_console': {
@@ -210,6 +241,10 @@ def _init_core():
'default': 'ipmi',
}),
},
'description': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'events': {
'hardware': {
'log': PluginRoute({
@@ -311,6 +346,12 @@ def _init_core():
},
},
'support': {
'servicedata': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
}
nodegroupresources = {
@@ -544,6 +585,15 @@ class BadPlugin(object):
self.node, self.plugin + ' is not a supported plugin')
class BadCollective(object):
def __init__(self, node):
self.node = node
def error(self, *args, **kwargs):
yield msg.ConfluentNodeError(
self.node, 'collective mode is active, but collective.manager '
'is not set for this node')
def abbreviate_noderange(configmanager, inputdata, operation):
if operation != 'create':
raise exc.InvalidArgumentException('Must be a create with nodes in list')
@@ -554,6 +604,63 @@ def abbreviate_noderange(configmanager, inputdata, operation):
return (msg.KeyValueData({'noderange': noderange.ReverseNodeRange(inputdata['nodes'], configmanager).noderange}),)
def handle_dispatch(connection, cert, dispatch, peername):
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
if not util.cert_matches(
cfm.get_collective_member(peername)['fingerprint'], cert):
connection.close()
return
dispatch = pickle.loads(dispatch)
configmanager = cfm.ConfigManager(dispatch['tenant'])
nodes = dispatch['nodes']
inputdata = dispatch['inputdata']
operation = dispatch['operation']
pathcomponents = dispatch['path']
routespec = nested_lookup(noderesources, pathcomponents)
plugroute = routespec.routeinfo
plugpath = None
nodesbyhandler = {}
passvalues = []
nodeattr = configmanager.get_node_attributes(
nodes, plugroute['pluginattrs'])
for node in nodes:
for attrname in plugroute['pluginattrs']:
if attrname in nodeattr[node]:
plugpath = nodeattr[node][attrname]['value']
elif 'default' in plugroute:
plugpath = plugroute['default']
if plugpath is not None:
try:
hfunc = getattr(pluginmap[plugpath], operation)
except KeyError:
nodesbyhandler[BadPlugin(node, plugpath).error] = [node]
continue
if hfunc in nodesbyhandler:
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
try:
for hfunc in nodesbyhandler:
passvalues.append(hfunc(
nodes=nodesbyhandler[hfunc], element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata))
for res in itertools.chain(*passvalues):
_forward_rsp(connection, res)
except Exception as res:
_forward_rsp(connection, res)
connection.sendall('\x00\x00\x00\x00\x00\x00\x00\x00')
def _forward_rsp(connection, res):
r = pickle.dumps(res)
rlen = len(r)
if not rlen:
return
connection.sendall(struct.pack('!Q', rlen))
connection.sendall(r)
def handle_node_request(configmanager, inputdata, operation,
pathcomponents, autostrip=True):
iscollection = False
@@ -633,7 +740,7 @@ def handle_node_request(configmanager, inputdata, operation,
else:
raise Exception("TODO here")
del pathcomponents[0:2]
passvalues = []
passvalues = queue.Queue()
plugroute = routespec.routeinfo
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes, isnoderange,
@@ -650,20 +757,36 @@ def handle_node_request(configmanager, inputdata, operation,
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
return passvalue
return [passvalue]
else:
return stripnode(passvalue, nodes[0])
elif 'pluginattrs' in plugroute:
nodeattr = configmanager.get_node_attributes(
nodes, plugroute['pluginattrs'])
nodes, plugroute['pluginattrs'] + ['collective.manager'])
plugpath = None
if 'default' in plugroute:
plugpath = plugroute['default']
nodesbymanager = {}
nodesbyhandler = {}
badcollnodes = []
for node in nodes:
for attrname in plugroute['pluginattrs']:
if attrname in nodeattr[node]:
plugpath = nodeattr[node][attrname]['value']
elif 'default' in plugroute:
plugpath = plugroute['default']
if plugpath in dispatch_plugins:
cfm.check_quorum()
manager = nodeattr[node].get('collective.manager', {}).get(
'value', None)
if manager:
if collective.get_myname() != manager:
if manager not in nodesbymanager:
nodesbymanager[manager] = set([node])
else:
nodesbymanager[manager].add(node)
continue
elif list(cfm.list_collective()):
badcollnodes.append(node)
continue
if plugpath is not None:
try:
hfunc = getattr(pluginmap[plugpath], operation)
@@ -674,19 +797,27 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
for bn in badcollnodes:
nodesbyhandler[BadCollective(bn).error] = [bn]
workers = greenpool.GreenPool()
numworkers = 0
for hfunc in nodesbyhandler:
passvalues.append(hfunc(
nodes=nodesbyhandler[hfunc], element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata))
numworkers += 1
workers.spawn(addtoqueue, passvalues, hfunc, {'nodes': nodesbyhandler[hfunc],
'element': pathcomponents,
'configmanager': configmanager,
'inputdata': inputdata})
for manager in nodesbymanager:
numworkers += 1
workers.spawn(addtoqueue, passvalues, dispatch_request, {
'nodes': nodesbymanager[manager], 'manager': manager,
'element': pathcomponents, 'configmanager': configmanager,
'inputdata': inputdata, 'operation': operation})
if isnoderange or not autostrip:
return itertools.chain(*passvalues)
return iterate_queue(numworkers, passvalues)
else:
if len(passvalues) > 0:
if isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
if numworkers > 0:
return iterate_queue(numworkers, passvalues, nodes[0])
else:
raise exc.NotImplementedException()
@@ -696,6 +827,117 @@ def handle_node_request(configmanager, inputdata, operation,
# return stripnode(passvalues[0], nodes[0])
def iterate_queue(numworkers, passvalues, strip=False):
completions = 0
while completions < numworkers:
nv = passvalues.get()
if nv == 'theend':
completions += 1
else:
if isinstance(nv, Exception):
raise nv
if strip and not isinstance(nv, console.Console):
nv.strip_node(strip)
yield nv
def addtoqueue(theq, fun, kwargs):
try:
result = fun(**kwargs)
if isinstance(result, console.Console):
theq.put(result)
else:
for pv in result:
theq.put(pv)
except Exception as e:
theq.put(e)
finally:
theq.put('theend')
def dispatch_request(nodes, manager, element, configmanager, inputdata,
operation):
a = configmanager.get_collective_member(manager)
try:
remote = socket.create_connection((a['address'], 13001))
remote.settimeout(90)
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
except Exception:
for node in nodes:
if a:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} is unreachable'.format(
a['name']))
else:
yield msg.ConfluentResourceUnavailable(
node,
'"{0}" is not recognized as a collective member'.format(
manager))
return
if not util.cert_matches(a['fingerprint'], remote.getpeercert(
binary_form=True)):
raise Exception("Invalid certificate on peer")
tlvdata.recv(remote)
tlvdata.recv(remote)
myname = collective.get_myname()
dreq = pickle.dumps({'name': myname, 'nodes': list(nodes),
'path': element,'tenant': configmanager.tenant,
'operation': operation, 'inputdata': inputdata})
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote.sendall(dreq)
while True:
try:
rlen = remote.recv(8)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rlen) < 8:
try:
nlen = remote.recv(8 - len(rlen))
except Exception:
nlen = 0
if not nlen:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rlen += nlen
rlen = struct.unpack('!Q', rlen)[0]
if rlen == 0:
break
try:
rsp = remote.recv(rlen)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rsp) < rlen:
try:
nrsp = remote.recv(rlen - len(rsp))
except Exception:
nrsp = 0
if not nrsp:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rsp += nrsp
rsp = pickle.loads(rsp)
if isinstance(rsp, Exception):
raise rsp
yield rsp
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
if pathcomponents[0] == 'detected':
pass
+74 -26
View File
@@ -85,6 +85,9 @@ import eventlet
import eventlet.greenpool
import eventlet.semaphore
autosensors = set()
scanner = None
class nesteddict(dict):
def __missing__(self, key):
@@ -350,7 +353,28 @@ def _parameterize_path(pathcomponents):
return validselectors, keyparams, listrequested, childcoll
def handle_autosense_config(operation, inputdata):
autosense = cfm.get_global('discovery.autosense')
autosense = autosense or autosense is None
if operation == 'retrieve':
yield msg.KeyValueData({'enabled': autosense})
elif operation == 'update':
enabled = inputdata['enabled']
if type(enabled) in (unicode, str):
enabled = enabled.lower() in ('true', '1', 'y', 'yes', 'enable',
'enabled')
if autosense == enabled:
return
cfm.set_global('discovery.autosense', enabled)
if enabled:
start_autosense()
else:
stop_autosense()
def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if pathcomponents == ['discovery', 'autosense']:
return handle_autosense_config(operation, inputdata)
if operation == 'retrieve':
return handle_read_api_request(pathcomponents)
elif (operation in ('update', 'create') and
@@ -359,6 +383,7 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
raise exc.InvalidArgumentException()
rescan()
return (msg.KeyValueData({'rescan': 'started'}),)
elif operation in ('update', 'create'):
if 'node' not in inputdata:
raise exc.InvalidArgumentException('Missing node name in input')
@@ -394,10 +419,13 @@ def handle_read_api_request(pathcomponents):
# TODO(jjohnson2): This should be more generalized...
# odd indexes into components are 'by-'*, even indexes
# starting at 2 are parameters to previous index
if pathcomponents == ['discovery', 'rescan']:
return (msg.KeyValueData({'scanning': bool(scanner)}),)
subcats, queryparms, indexof, coll = _parameterize_path(pathcomponents[1:])
if len(pathcomponents) == 1:
dirlist = [msg.ChildCollection(x + '/') for x in sorted(list(subcats))]
dirlist.append(msg.ChildCollection('rescan'))
dirlist.append(msg.ChildCollection('autosense'))
return dirlist
if not coll:
return show_info(queryparms['by-mac'])
@@ -695,6 +723,9 @@ def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
smmaddr = cd[nodename]['hardwaremanagement.manager']['value']
pkey = cd[nodename].get('pubkeys.tls_hardwaremanager', {}).get(
'value', None)
if not pkey:
# We cannot continue through a break in the chain
return None, False
if pkey:
cv = util.TLSCertVerifier(
cfg, nodename, 'pubkeys.tls_hardwaremanager').verify_cert
@@ -715,6 +746,8 @@ def get_smm_neighbor_fingerprints(smmaddr, cv):
smmaddr = '[{0}]'.format(smmaddr)
wc = webclient.SecureHTTPConnection(smmaddr, verifycallback=cv)
neighs = wc.grab_json_response('/scripts/neighdata.json')
if not neighs:
return
for idx in (4, 5):
if 'sha256' not in neighs[idx]:
continue
@@ -867,8 +900,9 @@ def eval_node(cfg, handler, info, nodename, manual=False):
if enl:
# ambiguous SMM situation according to the configuration, we need
# to match uuid
encuuid = info['attributes'].get('chasis-uuid', None)
encuuid = info['attributes'].get('chassis-uuid', None)
if encuuid:
encuuid = encuuid[0]
enl = list(cfg.filter_node_attributes('id.uuid=' + encuuid))
if len(enl) != 1:
# errorstr = 'No SMM by given UUID known, *yet*'
@@ -924,26 +958,25 @@ def eval_node(cfg, handler, info, nodename, manual=False):
# but... is this really ok? could be on an upstream port or
# erroneously put in the enclosure with no nodes yet
# so first, see if the candidate node is a chain host
if info.get('maccount', False):
# discovery happened through switch
nl = list(cfg.filter_node_attributes(
'enclosure.extends=' + nodename))
if nl:
# The candidate nodename is the head of a chain, we must
# validate the smm certificate by the switch
macmap.get_node_fingerprint(nodename, cfg)
util.handler.cert_matches(fprint, handler.https_cert)
if not manual:
if info.get('maccount', False):
# discovery happened through switch
nl = list(cfg.filter_node_attributes(
'enclosure.extends=' + nodename))
if nl:
# The candidate nodename is the head of a chain, we must
# validate the smm certificate by the switch
macmap.get_node_fingerprint(nodename, cfg)
util.handler.cert_matches(fprint, handler.https_cert)
return
if (info.get('maccount', False) and
not handler.discoverable_by_switch(info['maccount'])):
errorstr = 'The detected node {0} was detected using switch, ' \
'however the relevant port has too many macs learned ' \
'for this type of device ({1}) to be discovered by ' \
'switch.'.format(nodename, handler.devname)
log.log({'error': errorstr})
return
if (info.get('maccount', False) and
not handler.discoverable_by_switch(info['maccount'])):
errorstr = 'The detected node {0} was detected using switch, ' \
'however the relevant port has too many macs learned ' \
'for this type of device ({1}) to be discovered by ' \
'switch.'.format(nodename, handler.devname)
if manual:
raise exc.InvalidArgumentException(errorstr)
log.log({'error': errorstr})
return
if not discover_node(cfg, handler, info, nodename, manual):
pending_nodes[nodename] = info
@@ -1075,12 +1108,14 @@ def newnodes(added, deleting, configmanager):
global attribwatcher
global needaddhandled
global nodeaddhandler
_map_unique_ids()
configmanager.remove_watcher(attribwatcher)
allnodes = configmanager.list_nodes()
attribwatcher = configmanager.watch_attributes(
allnodes, ('discovery.policy', 'net*.switch',
'hardwaremanagement.manager', 'net*.switchport', 'id.uuid',
'pubkeys.tls_hardwaremanager', 'net*.bootable'), _recheck_nodes)
'hardwaremanagement.manager', 'net*.switchport',
'id.uuid', 'pubkeys.tls_hardwaremanager',
'net*.bootable'), _recheck_nodes)
if nodeaddhandler:
needaddhandled = True
else:
@@ -1112,7 +1147,11 @@ def _periodic_recheck(configmanager):
def rescan():
_map_unique_ids()
eventlet.spawn_n(slp.active_scan, safe_detected)
global scanner
if scanner:
return
else:
scanner = eventlet.spawn(slp.active_scan, safe_detected)
def start_detection():
@@ -1126,14 +1165,23 @@ def start_detection():
'hardwaremanagement.manager', 'net*.switchport', 'id.uuid',
'pubkeys.tls_hardwaremanager'), _recheck_nodes)
cfg.watch_nodecollection(newnodes)
eventlet.spawn_n(slp.snoop, safe_detected)
eventlet.spawn_n(pxe.snoop, safe_detected)
autosense = cfm.get_global('discovery.autosense')
if autosense or autosense is None:
start_autosense()
if rechecker is None:
rechecktime = util.monotonic_time() + 900
rechecker = eventlet.spawn_after(900, _periodic_recheck, cfg)
# eventlet.spawn_n(ssdp.snoop, safe_detected)
def stop_autosense():
for watcher in list(autosensors):
watcher.kill()
autosensors.discard(watcher)
def start_autosense():
autosensors.add(eventlet.spawn(slp.snoop, safe_detected))
autosensors.add(eventlet.spawn(pxe.snoop, safe_detected))
nodes_by_fprint = {}
@@ -1155,7 +1203,7 @@ def _map_unique_ids(nodes=None):
uuid_by_nodes = {}
fprint_by_nodes = {}
for uuid in nodes_by_uuid:
if not uuid_is_valid():
if not uuid_is_valid(uuid):
continue
node = nodes_by_uuid[uuid]
if node in bigmap:
@@ -45,7 +45,7 @@ class NodeHandler(generic.NodeHandler):
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(self, nodename, reset=False):
def _bmcconfig(self, nodename, reset=False, customconfig=None):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
@@ -74,6 +74,8 @@ class NodeHandler(generic.NodeHandler):
ic = self._get_ipmicmd(user, passwd)
else:
raise
if customconfig:
customconfig(ic)
currusers = ic.get_users()
lanchan = ic.get_network_channel()
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
@@ -122,16 +124,18 @@ class NodeHandler(generic.NodeHandler):
if currusers[uid]['name'] == newuser:
# Use existing account that has been created
newuserslot = uid
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
break
else:
newuserslot = lockedusers + 1
if newuserslot < 2:
newuserslot = 2
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
ic.set_user_name(newuserslot, newuser)
ic.set_user_access(newuserslot, lanchan,
privilege_level='administrator')
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
# Now to zap others
for uid in currusers:
if uid != newuserslot:
@@ -13,11 +13,13 @@
# limitations under the License.
import confluent.discovery.handlers.imm as immhandler
import confluent.util as util
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.oem.lenovo.imm as imm
class NodeHandler(immhandler.NodeHandler):
devname = 'XCC'
@@ -41,16 +43,45 @@ class NodeHandler(immhandler.NodeHandler):
#if ipmicmd:
# ipmicmd.ipmi_session.logout()
def validate_cert(self, certificate):
# broadly speaking, merely checks consistency moment to moment,
# but if https_cert gets stricter, this check means something
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def set_password_policy(self, ic):
ruleset = {'USER_GlobalMinPassChgInt': '0'}
for rule in self.ruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
if name.lower() in ('expiry', 'expiration'):
ruleset['USER_GlobalPassExpPeriod'] = value
if int(value) < 5:
ruleset['USER_GlobalPassExpWarningPeriod'] = value
if name.lower() in ('lockout', 'loginfailures'):
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
ruleset['USER_GlobalMaxLoginFailures'] = value
ic.register_key_handler(self.validate_cert)
ic.oem_init()
ic._oem.immhandler.wc.grab_json_response('/api/dataset', ruleset)
def config(self, nodename, reset=False):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
ic = self._bmcconfig(nodename)
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
# Ok, we can get the enclosure uuid now..
ic.oem_init()
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
enclosureuuid = ic._oem.immhandler.get_property(
@@ -16,13 +16,13 @@
import confluent.neighutil as neighutil
import confluent.util as util
import confluent.log as log
import os
import random
import eventlet.green.select as select
import eventlet.green.socket as socket
import struct
import subprocess
import traceback
_slp_services = set([
'service:management-hardware.IBM:integrated-management-module2',
@@ -391,6 +391,7 @@ def snoop(handler):
:param handler:
:return:
"""
tracelog = log.Logger('trace')
active_scan(handler)
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
@@ -420,54 +421,58 @@ def snoop(handler):
net4.bind(('', 427))
while True:
newmacs = set([])
r, _, _ = select.select((net, net4), (), (), 60)
# clear known_peers and peerbymacaddress
# to avoid stale info getting in...
# rely upon the select(0.2) to catch rapid fire and aggregate ip
# addresses that come close together
# calling code needs to understand deeper context, as snoop
# will now yield dupe info over time
known_peers = set([])
peerbymacaddress = {}
neighutil.update_neigh()
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
ip = peer[0].partition('%')[0]
if ip not in neighutil.neightable:
continue
if peer in known_peers:
continue
known_peers.add(peer)
mac = neighutil.neightable[ip]
if mac in peerbymacaddress:
peerbymacaddress[mac]['addresses'].append(peer)
else:
q = query_srvtypes(peer)
if not q or not q[0]:
# SLP might have started and not ready yet
# ignore for now
known_peers.discard(peer)
try:
newmacs = set([])
r, _, _ = select.select((net, net4), (), (), 60)
# clear known_peers and peerbymacaddress
# to avoid stale info getting in...
# rely upon the select(0.2) to catch rapid fire and aggregate ip
# addresses that come close together
# calling code needs to understand deeper context, as snoop
# will now yield dupe info over time
known_peers = set([])
peerbymacaddress = {}
neighutil.update_neigh()
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
ip = peer[0].partition('%')[0]
if ip not in neighutil.neightable:
continue
# we want to prioritize the very well known services
svcs = []
for svc in q:
if svc in _slp_services:
svcs.insert(0, svc)
else:
svcs.append(svc)
peerbymacaddress[mac] = {
'services': svcs,
'addresses': [peer],
}
newmacs.add(mac)
r, _, _ = select.select((net, net4), (), (), 0.2)
for mac in newmacs:
peerbymacaddress[mac]['xid'] = 1
_add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
handler(peerbymacaddress[mac])
if peer in known_peers:
continue
known_peers.add(peer)
mac = neighutil.neightable[ip]
if mac in peerbymacaddress:
peerbymacaddress[mac]['addresses'].append(peer)
else:
q = query_srvtypes(peer)
if not q or not q[0]:
# SLP might have started and not ready yet
# ignore for now
known_peers.discard(peer)
continue
# we want to prioritize the very well known services
svcs = []
for svc in q:
if svc in _slp_services:
svcs.insert(0, svc)
else:
svcs.append(svc)
peerbymacaddress[mac] = {
'services': svcs,
'addresses': [peer],
}
newmacs.add(mac)
r, _, _ = select.select((net, net4), (), (), 0.2)
for mac in newmacs:
peerbymacaddress[mac]['xid'] = 1
_add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
handler(peerbymacaddress[mac])
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def active_scan(handler):
@@ -191,7 +191,6 @@ def _parse_ssdp(peer, rsp, peerdata):
_, code, _ = headlines[0].split(' ', 2)
except ValueError:
return
myurl = None
if code == '200':
if nid in peerdata:
peerdatum = peerdata[nid]
@@ -208,7 +207,6 @@ def _parse_ssdp(peer, rsp, peerdata):
header = header.strip()
value = value.strip()
if header == 'AL' or header == 'LOCATION':
myurl = value
if 'urls' not in peerdatum:
peerdatum['urls'] = [value]
elif value not in peerdatum['urls']:
+6
View File
@@ -68,6 +68,11 @@ class LockedCredentials(ConfluentException):
_apierrorstr = 'Credential store locked'
class DegradedCollective(ConfluentException):
# We are in a collective with at least half of the member nodes missing
_apierrorstr = 'Collective does not have quorum'
class ForbiddenRequest(ConfluentException):
# The client request is not allowed by authorization engine
apierrorcode = 403
@@ -101,6 +106,7 @@ class PubkeyInvalid(ConfluentException):
super(PubkeyInvalid, self).__init__(self, text)
self.fingerprint = fingerprint
self.attrname = attribname
self.message = text
bodydata = {'message': text,
'event': event,
'fingerprint': fingerprint,
+25 -3
View File
@@ -21,13 +21,25 @@
import confluent.exceptions as exc
import confluent.messages as msg
import eventlet
import os
import pwd
import socket
updatesbytarget = {}
uploadsbytarget = {}
downloadsbytarget = {}
updatepool = eventlet.greenpool.GreenPool(256)
def execupdate(handler, filename, updateobj, type):
def execupdate(handler, filename, updateobj, type, owner, node):
if type != 'ffdc' and not os.path.exists(filename):
errstr = '{0} does not appear to exist on {1}'.format(
filename, socket.gethostname())
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
return
if type == 'ffdc' and os.path.isdir(filename):
filename += '/' + node + '.svcdata'
try:
if type == 'firmware':
completion = handler(filename, progress=updateobj.handle_progress,
@@ -36,6 +48,9 @@ def execupdate(handler, filename, updateobj, type):
completion = handler(filename, progress=updateobj.handle_progress)
if completion is None:
completion = 'complete'
if owner:
pwent = pwd.getpwnam(owner)
os.chown(filename, pwent.pw_uid, pwent.pw_gid)
updateobj.handle_progress({'phase': completion, 'progress': 100.0})
except exc.PubkeyInvalid as pi:
errstr = 'Certificate mismatch detected, does not match value in ' \
@@ -48,18 +63,20 @@ def execupdate(handler, filename, updateobj, type):
class Updater(object):
def __init__(self, node, handler, filename, tenant=None, name=None,
bank=None, type='firmware'):
bank=None, type='firmware', owner=None):
self.bank = bank
self.node = node
self.phase = 'initializing'
self.detail = ''
self.percent = 0.0
self.updateproc = updatepool.spawn(execupdate, handler, filename,
self, type)
self, type, owner, node)
if type == 'firmware':
myparty = updatesbytarget
elif type == 'mediaupload':
myparty = uploadsbytarget
elif type == 'ffdc':
myparty = downloadsbytarget
if (node, tenant) not in myparty:
myparty[(node, tenant)] = {}
if name is None:
@@ -89,6 +106,8 @@ def remove_updates(nodes, tenant, element, type='firmware'):
upid = element[-1]
if type == 'firmware':
myparty = updatesbytarget
elif type == 'ffdc':
myparty = downloadsbytarget
else:
myparty = uploadsbytarget
for node in nodes:
@@ -108,6 +127,9 @@ def list_updates(nodes, tenant, element, type='firmware'):
if type == 'mediaupload':
myparty = uploadsbytarget
verb = 'upload'
elif type == 'ffdc':
verb = 'download'
myparty = downloadsbytarget
else:
myparty = updatesbytarget
verb = 'update'
+1 -1
View File
@@ -785,7 +785,7 @@ def serve(bind_host, bind_port):
' a second\n')
eventlet.sleep(1)
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
debug=False)
debug=False, socket_timeout=60)
class HttpApi(object):
+2
View File
@@ -33,6 +33,7 @@ import confluent.consoleserver as consoleserver
import confluent.core as confluentcore
import confluent.httpapi as httpapi
import confluent.log as log
import confluent.collective.manager as collective
try:
import confluent.sockapi as sockapi
except ImportError:
@@ -228,6 +229,7 @@ def run():
_updatepidfile()
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
collective.startup()
if dbgif:
oumask = os.umask(0077)
try:
+45
View File
@@ -397,6 +397,9 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
elif (path[:3] == ['configuration', 'management_controller', 'identifier']
and operation != 'retrieve'):
return InputMCI(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'management_controller', 'hostname']
and operation != 'retrieve'):
return InputHostname(path, nodes, inputdata, configmanager)
elif (path[:4] == ['configuration', 'management_controller',
'net_interfaces', 'management'] and operation != 'retrieve'):
return InputNetworkConfiguration(path, nodes, inputdata,
@@ -413,12 +416,17 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
elif (path[:3] == ['configuration', 'system', 'all'] and
operation != 'retrieve'):
return InputConfigChangeSet(path, inputdata, nodes, configmanager)
elif (path[:3] == ['configuration', 'system', 'clear'] and
operation != 'retrieve'):
return InputConfigClear(path, inputdata, nodes, configmanager)
elif 'inventory/firmware/updates/active' in '/'.join(path) and inputdata:
return InputFirmwareUpdate(path, nodes, inputdata)
elif '/'.join(path).startswith('media/detach'):
return DetachMedia(path, nodes, inputdata)
elif '/'.join(path).startswith('media/') and inputdata:
return InputMedia(path, nodes, inputdata)
elif '/'.join(path).startswith('support/servicedata') and inputdata:
return InputMedia(path, nodes, inputdata)
elif inputdata:
raise exc.InvalidArgumentException(
'No known input handler for request')
@@ -487,6 +495,13 @@ class InputExpression(ConfluentMessage):
nodeattr = deepcopy(self.nodeattribs[node])
return nodeattr
class InputConfigClear(ConfluentMessage):
def __init__(self, path, inputdata, nodes=None, configmanager=None):
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if 'clear' not in inputdata or not inputdata['clear']:
raise exc.InvalidArgumentException('Input must be {"clear":true}')
class InputConfigChangeSet(InputExpression):
# For now, this is identical to InputExpression, later it may
# internalize formula expansion, but not now..
@@ -719,6 +734,25 @@ class InputBMCReset(ConfluentInputMessage):
return self.inputbynode[node]
class InputHostname(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata, configmanager):
self.inputbynode = {}
self.stripped = False
if not inputdata or 'hostname' not in inputdata:
raise exc.InvalidArgumentException('missing hostname attribute')
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
for expanded in configmanager.expand_attrib_expression(
nodes, inputdata['hostname']):
node, value = expanded
self.inputbynode[node] = value
def hostname(self, node):
return self.inputbynode[node]
class InputMCI(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
self.inputbynode = {}
@@ -1298,6 +1332,17 @@ class MCI(ConfluentMessage):
self.kvpairs = {name: kv}
class Hostname(ConfluentMessage):
def __init__(self, name=None, hostname=None):
self.notnode = name is None
self.desc = 'BMC hostname'
kv = {'hostname': {'value': hostname}}
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
+25 -1
View File
@@ -122,4 +122,28 @@ def get_prefix_len_for_ip(ip):
nbits += 1
maskn = maskn << 1 & 0xffffffff
return nbits
raise exc.NotImplementedException("Non local addresses not supported")
raise exc.NotImplementedException("Non local addresses not supported")
def addresses_match(addr1, addr2):
"""Check two network addresses for similarity
Is it zero padded in one place, not zero padded in another? Is one place by name and another by IP??
Is one context getting a normal IPv4 address and another getting IPv4 in IPv6 notation?
This function examines the two given names, performing the required changes to compare them for equivalency
:param addr1:
:param addr2:
:return: True if the given addresses refer to the same thing
"""
for addrinfo in socket.getaddrinfo(addr1, 0, 0, socket.SOCK_STREAM):
rootaddr1 = socket.inet_pton(addrinfo[0], addrinfo[4][0])
if addrinfo[0] == socket.AF_INET6 and rootaddr1[:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
# normalize to standard IPv4
rootaddr1 = rootaddr1[-4:]
for otherinfo in socket.getaddrinfo(addr2, 0, 0, socket.SOCK_STREAM):
otheraddr = socket.inet_pton(otherinfo[0], otherinfo[4][0])
if otherinfo[0] == socket.AF_INET6 and otheraddr[:12] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff':
otheraddr = otheraddr[-4:]
if otheraddr == rootaddr1:
return True
return False
+32 -16
View File
@@ -88,7 +88,7 @@ _chassisidbyswitch = {}
def lenovoname(idx, desc):
if desc.isdigit():
return 'Ethernet' + str(idx)
return 'Ethernet' + str(desc)
return desc
nameoverrides = [
@@ -151,6 +151,9 @@ def _extract_extended_desc(info, source, integritychecked):
info['peerdescription'] = source
def sanitize(val):
# This is pretty much the same approach net-snmp takes.
# if the string is printable as-is, then just give it as-is
# if the string has non-printable, then hexify it
val = str(val)
for x in val.strip('\x00'):
if ord(x) < 32 or ord(x) > 128:
@@ -161,14 +164,14 @@ def sanitize(val):
def _init_lldp(data, iname, idx, idxtoportid, switch):
if iname not in data:
data[iname] = {'port': iname, 'portid': str(idxtoportid[idx]),
'chassisid': str(_chassisidbyswitch[switch])}
'chassisid': _chassisidbyswitch[switch]}
def _extract_neighbor_data_b(args):
"""Build LLDP data about elements connected to switch
args are carried as a tuple, because of eventlet convenience
"""
switch, password, user, force = args
switch, password, user, force = args[:4]
vintage = _neighdata.get(switch, {}).get('!!vintage', 0)
now = util.monotonic_time()
if vintage > (now - 60) and not force:
@@ -180,7 +183,8 @@ def _extract_neighbor_data_b(args):
sid = str(sysid[1][6:])
idxtoifname = {}
idxtoportid = {}
_chassisidbyswitch[switch] = list(conn.walk('1.0.8802.1.1.2.1.3.2'))[0][1]
_chassisidbyswitch[switch] = sanitize(list(
conn.walk('1.0.8802.1.1.2.1.3.2'))[0][1])
for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.3'):
idx = oidindex[0][-1]
idxtoportid[idx] = sanitize(oidindex[1])
@@ -216,17 +220,19 @@ def _extract_neighbor_data_b(args):
_neighdata[switch] = lldpdata
def update_switch_data(switch, configmanager, force=False):
def update_switch_data(switch, configmanager, force=False, retexc=False):
switchcreds = netutil.get_switchcreds(configmanager, (switch,))[0]
_extract_neighbor_data(switchcreds + (force,))
ndr = _extract_neighbor_data(switchcreds + (force, retexc))
if retexc and isinstance(ndr, Exception):
raise ndr
return _neighdata.get(switch, {})
def update_neighbors(configmanager, force=False):
return _update_neighbors_backend(configmanager, force)
def update_neighbors(configmanager, force=False, retexc=False):
return _update_neighbors_backend(configmanager, force, retexc)
def _update_neighbors_backend(configmanager, force):
def _update_neighbors_backend(configmanager, force, retexc):
global _neighdata
global _neighbypeerid
vintage = _neighdata.get('!!vintage', 0)
@@ -237,7 +243,7 @@ def _update_neighbors_backend(configmanager, force):
_neighbypeerid = {'!!vintage': now}
switches = netutil.list_switches(configmanager)
switchcreds = netutil.get_switchcreds(configmanager, switches)
switchcreds = [ x + (force,) for x in switchcreds]
switchcreds = [ x + (force, retexc) for x in switchcreds]
pool = GreenPool(64)
for ans in pool.imap(_extract_neighbor_data, switchcreds):
yield ans
@@ -254,9 +260,15 @@ def _extract_neighbor_data(args):
return
try:
with _updatelocks[switch]:
_extract_neighbor_data_b(args)
except Exception:
log.logtrace()
return _extract_neighbor_data_b(args)
except Exception as e:
yieldexc = False
if len(args) >= 5:
yieldexc = args[4]
if yieldexc:
return e
else:
log.logtrace()
if __name__ == '__main__':
# a quick one-shot test, args are switch and snmpv1 string for now
@@ -323,7 +335,9 @@ def _handle_neighbor_query(pathcomponents, configmanager):
# guaranteed
if (parms['by-peerid'] not in _neighbypeerid and
_neighbypeerid.get('!!vintage', 0) < util.monotonic_time() - 60):
list(update_neighbors(configmanager))
for x in update_neighbors(configmanager, retexc=True):
if isinstance(x, Exception):
raise x
if parms['by-peerid'] not in _neighbypeerid:
raise exc.NotFoundException('No matching peer known')
return _dump_neighbordatum(_neighbypeerid[parms['by-peerid']])
@@ -332,9 +346,11 @@ def _handle_neighbor_query(pathcomponents, configmanager):
if listrequested not in multi_selectors | single_selectors:
raise exc.NotFoundException('{0} is not found'.format(listrequested))
if 'by-switch' in parms:
update_switch_data(parms['by-switch'], configmanager)
update_switch_data(parms['by-switch'], configmanager, retexc=True)
else:
list(update_neighbors(configmanager))
for x in update_neighbors(configmanager, retexc=True):
if isinstance(x, Exception):
raise x
return list_info(parms, listrequested)
@@ -47,6 +47,7 @@ import eventlet.semaphore
import re
_macmap = {}
_apimacmap = {}
_macsbyswitch = {}
_nodesbymac = {}
_switchportmap = {}
@@ -211,7 +212,7 @@ def _map_switch_backend(args):
maccounts[ifname] = 1
else:
maccounts[ifname] += 1
_macsbyswitch[switch] = {}
newmacs = {}
for mac in mactobridge:
# We want to merge it so that when a mac appears in multiple
# places, it is captured.
@@ -223,10 +224,10 @@ def _map_switch_backend(args):
_macmap[mac].append((switch, ifname, maccounts[ifname]))
else:
_macmap[mac] = [(switch, ifname, maccounts[ifname])]
if ifname in _macsbyswitch[switch]:
_macsbyswitch[switch][ifname].append(mac)
if ifname in newmacs:
newmacs[ifname].append(mac)
else:
_macsbyswitch[switch][ifname] = [mac]
newmacs[ifname] = [mac]
nodename = _nodelookup(switch, ifname)
if nodename is not None:
if mac in _nodesbymac and _nodesbymac[mac][0] != nodename:
@@ -238,6 +239,7 @@ def _map_switch_backend(args):
_nodesbymac[mac] = (None, None)
else:
_nodesbymac[mac] = (nodename, maccounts[ifname])
_macsbyswitch[switch] = newmacs
switchbackoff = 30
@@ -295,6 +297,7 @@ def _finish_update(completions):
def _full_updatemacmap(configmanager):
global vintage
global _apimacmap
global _macmap
global _nodesbymac
global _switchportmap
@@ -307,7 +310,6 @@ def _full_updatemacmap(configmanager):
_macmap = {}
_nodesbymac = {}
_switchportmap = {}
_macsbyswitch = {}
if configmanager.tenant is not None:
raise exc.ForbiddenRequest(
'Network topology not available to tenants')
@@ -340,11 +342,15 @@ def _full_updatemacmap(configmanager):
_switchportmap[curswitch][portname] = None
else:
_switchportmap[curswitch][portname] = node
for switch in _macsbyswitch:
if switch not in switches:
del _macsbyswitch[switch]
switchauth = get_switchcreds(configmanager, switches)
pool = GreenPool(64)
for ans in pool.imap(_map_switch, switchauth):
vintage = util.monotonic_time()
yield ans
_apimacmap = _macmap
endtime = util.monotonic_time()
duration = endtime - start
duration = duration * 15 # wait 15 times as long as it takes to walk
@@ -424,7 +430,7 @@ def handle_read_api_request(pathcomponents, configmanager):
elif pathcomponents[2] == 'by-mac':
if len(pathcomponents) == 3:
return [msg.ChildCollection(x.replace(':', '-'))
for x in sorted(list(_macmap))]
for x in sorted(list(_apimacmap))]
elif len(pathcomponents) == 4:
return dump_macinfo(pathcomponents[-1])
elif pathcomponents[2] == 'by-switch':
@@ -457,12 +463,14 @@ def handle_read_api_request(pathcomponents, configmanager):
for x in sorted(maclist)]
if len(pathcomponents) == 8:
return dump_macinfo(pathcomponents[-1])
elif pathcomponents[2] == 'rescan':
return [msg.KeyValueData({'scanning': mapupdating.locked()})]
raise exc.NotFoundException('Unrecognized path {0}'.format(
'/'.join(pathcomponents)))
def dump_macinfo(macaddr):
macaddr = macaddr.replace('-', ':')
macaddr = macaddr.replace('-', ':').lower()
info = _macmap.get(macaddr, None)
if info is None:
raise exc.NotFoundException(
@@ -20,6 +20,7 @@ import confluent.util as util
def retrieve(nodes, element, configmanager, inputdata):
configmanager.check_quorum()
if nodes is not None:
return retrieve_nodes(nodes, element, configmanager, inputdata)
elif element[0] == 'nodegroups':
@@ -183,8 +184,10 @@ def _expand_expression(nodes, configmanager, inputdata):
pernodeexpressions[expanded[0]] = expanded[1]
for node in util.natural_sort(pernodeexpressions):
yield msg.KeyValueData({'value': pernodeexpressions[node]}, node)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
except (SyntaxError, ValueError) as e:
raise exc.InvalidArgumentException(
'Bad confluent expression syntax (must use "{{" and "}}" if not '
'desiring confluent expansion): ' + str(e))
@@ -19,12 +19,14 @@ import confluent.firmwaremanager as firmwaremanager
import confluent.interface.console as conapi
import confluent.messages as msg
import confluent.util as util
import copy
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
from fnmatch import fnmatch
import pyghmi.constants as pygconstants
import pyghmi.exceptions as pygexc
console = eventlet.import_patched('pyghmi.ipmi.console')
@@ -32,6 +34,37 @@ ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
import socket
import ssl
pci_cache = {}
def get_dns_txt(qstring):
return eventlet.support.greendns.resolver.query(
qstring, 'TXT')[0].strings[0].replace('i=', '')
def get_pci_text_from_ids(subdevice, subvendor, device, vendor):
fqpi = '{0}.{1}.{2}.{3}'.format(subdevice, subvendor, device, vendor)
if fqpi in pci_cache:
return pci_cache[fqpi]
vendorstr = None
try:
vendorstr = get_dns_txt('{0}.pci.id.ucw.cz'.format(subvendor))
except Exception:
try:
vendorstr = get_dns_txt('{0}.pci.id.ucw.cz'.format(vendor))
except Exception:
pass
devstr = None
try:
devstr = get_dns_txt(fqpi + '.pci.id.ucw.cz')
except Exception:
try:
devstr = get_dns_txt('{0}.{1}.pci.id.ucw.cz'.format(
device, vendor))
except Exception:
pass
if vendorstr and devstr:
pci_cache[fqpi] = vendorstr, devstr
return vendorstr, devstr
# There is something not right with the RLocks used in pyghmi when
# eventlet comes into play. It seems like sometimes on acquire,
@@ -132,7 +165,7 @@ class IpmiCommandWrapper(ipmicommand.Command):
self._inhealth = False
self._lasthealth = None
self._attribwatcher = cfm.watch_attributes(
(node,), ('secret.hardwaremanagementuser',
(node,), ('secret.hardwaremanagementuser', 'collective.manager',
'secret.hardwaremanagementpassword', 'secret.ipmikg',
'hardwaremanagement.manager'), self._attribschanged)
super(self.__class__, self).__init__(**kwargs)
@@ -270,13 +303,17 @@ class IpmiConsole(conapi.Console):
kg=self.kg, force=True,
iohandler=self.handle_data)
self.solconnection.outputlock = NullLock()
while not self.solconnection.connected and not self.broken:
while (self.solconnection and not self.solconnection.connected and
not (self.broken or self.solconnection.broken or
self.solconnection.ipmi_session.broken)):
w = eventlet.event.Event()
_ipmiwaiters.append(w)
w.wait()
if self.broken:
break
if self.broken:
w.wait(15)
if (self.broken or not self.solconnection or
self.solconnection.broken or
self.solconnection.ipmi_session.broken):
if not self.error:
self.error = 'Unknown error'
if (self.error.startswith('Incorrect password') or
self.error.startswith('Unauthorized name')):
raise exc.TargetEndpointBadCredentials
@@ -292,6 +329,7 @@ class IpmiConsole(conapi.Console):
self.solconnection.out_handler = _donothing
self.solconnection.close()
self.solconnection = None
self.datacallback = None
self.broken = True
self.error = "closed"
@@ -344,7 +382,7 @@ def perform_request(operator, node, element,
cfg, results).handle_request()
except pygexc.IpmiException as ipmiexc:
excmsg = str(ipmiexc)
if excmsg == 'Session no longer connected':
if excmsg in ('Session no longer connected', 'timeout'):
results.put(msg.ConfluentTargetTimeout(node))
else:
results.put(msg.ConfluentNodeError(node, excmsg))
@@ -380,6 +418,7 @@ class IpmiHandler(object):
self.error = None
eventlet.sleep(0)
self.cfg = cfd[node]
self.current_user = cfg.current_user
self.loggedin = False
self.node = node
self.element = element
@@ -390,7 +429,8 @@ class IpmiHandler(object):
self.tenant = cfg.tenant
tenant = cfg.tenant
if ((node, tenant) not in persistent_ipmicmds or
not persistent_ipmicmds[(node, tenant)].ipmi_session.logged):
not persistent_ipmicmds[(node, tenant)].ipmi_session.logged or
persistent_ipmicmds[(node, tenant)].ipmi_session.broken):
try:
persistent_ipmicmds[(node, tenant)].close_confluent()
except KeyError: # was no previous session
@@ -405,9 +445,10 @@ class IpmiHandler(object):
ipmisess = persistent_ipmicmds[(node, tenant)].ipmi_session
begin = util.monotonic_time()
while ((not (self.broken or self.loggedin)) and
(util.monotonic_time() - begin) < 180):
ipmisess.wait_for_rsp(180)
(util.monotonic_time() - begin) < 30):
ipmisess.wait_for_rsp(31 - (util.monotonic_time() - begin))
if not (self.broken or self.loggedin):
ipmisess._mark_broken()
raise exc.TargetEndpointUnreachable(
"Login process to " + connparams['bmc'] + " died")
except socket.gaierror as ge:
@@ -436,6 +477,10 @@ class IpmiHandler(object):
self.output.put(msg.ConfluentTargetTimeout(
self.node, self.error))
return
elif 'Invalid Session ID' in self.error:
self.output.put(msg.ConfluentTargetTimeout(
self.node, 'Temporary Login Error'))
return
elif ('Unauthorized' in self.error or
'Incorrect password' in self.error):
self.output.put(
@@ -475,6 +520,10 @@ class IpmiHandler(object):
self.decode_alert()
elif self.element == ['console', 'license']:
self.handle_license()
elif self.element == ['support', 'servicedata']:
self.handle_servicedata_fetch()
elif self.element == ['description']:
self.handle_description()
else:
raise Exception('Not Implemented')
@@ -494,6 +543,14 @@ class IpmiHandler(object):
self.output.put(msg.CreatedResource(
'nodes/{0}/media/uploads/{1}'.format(self.node, u.name)))
def handle_servicedata_fetch(self):
u = firmwaremanager.Updater(
self.node, self.ipmicmd.get_diagnostic_data,
self.inputdata.filename, self.tenant, type='ffdc',
owner=self.current_user)
self.output.put(msg.CreatedResource(
'nodes/{0}/support/servicedata/{1}'.format(self.node, u.name)))
def handle_attach_media(self):
try:
self.ipmicmd.attach_remote_media(self.inputdata.filename)
@@ -518,12 +575,18 @@ class IpmiHandler(object):
return self.handle_reset()
elif self.element[1:3] == ['management_controller', 'identifier']:
return self.handle_identifier()
elif self.element[1:3] == ['management_controller', 'hostname']:
return self.handle_hostname()
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()
elif self.element[1:3] == ['system', 'all']:
return self.handle_sysconfig()
elif self.element[1:3] == ['system', 'advanced']:
return self.handle_sysconfig(True)
elif self.element[1:3] == ['system', 'clear']:
return self.handle_sysconfigclear()
raise Exception('Not implemented')
def decode_alert(self):
@@ -595,10 +658,20 @@ class IpmiHandler(object):
))
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'])
try:
self.ipmicmd.set_net_configuration(
ipv4_address=config['ipv4_address'],
ipv4_configuration=config['ipv4_configuration'],
ipv4_gateway=config['ipv4_gateway'])
except socket.error as se:
self.output.put(msg.ConfluentNodeError(self.node,
se.message))
except ValueError as e:
if e.message == 'negative shift count':
self.output.put(msg.ConfluentNodeError(
self.node, 'Invalid prefix length given'))
else:
raise
def handle_users(self):
# Create user
@@ -800,12 +873,14 @@ class IpmiHandler(object):
if component == 'all':
for invdata in self.ipmicmd.get_inventory():
if invdata[1] is None:
newinf = {'present': False, 'information': None}
newinf = {'present': False, 'information': None,
'name': invdata[0]}
else:
sanitize_invdata(invdata[1])
newinf = {'present': True, 'information': invdata[1]}
newinf['name'] = invdata[0]
invitems.append(newinf)
newinf['name'] = invdata[1].get('name', invdata[0])
self.add_invitem(invitems, newinf)
else:
self.make_inventory_map()
compname = self.invmap.get(component, None)
@@ -814,12 +889,13 @@ class IpmiHandler(object):
return
invdata = self.ipmicmd.get_inventory_of_component(compname)
if invdata is None:
newinf = {'present': False, 'information': None}
newinf = {'present': False, 'information': None,
'name': compname}
else:
sanitize_invdata(invdata)
newinf = {'present': True, 'information': invdata}
newinf['name'] = compname
invitems.append(newinf)
newinf = {'present': True, 'information': invdata,
'name': invdata.get('name', compname)}
self.add_invitem(invitems, newinf)
except ssl.SSLEOFError:
errorneeded = msg.ConfluentNodeError(
self.node, 'Unable to communicate with the https server on '
@@ -836,6 +912,24 @@ class IpmiHandler(object):
if errorneeded:
self.output.put(errorneeded)
def add_invitem(self, invitems, newinf):
if newinf.get('information', None) and 'name' in newinf['information']:
newinf = copy.deepcopy(newinf)
del newinf['information']['name']
if fnmatch(newinf['name'], 'Adapter ??:??:??') or fnmatch(
newinf['name'], 'PCIeGen? x*'):
myinf = newinf.get('information', {})
sdid = myinf.get('PCI Subsystem Device ID', None)
svid = myinf.get('PCI Subsystem Vendor ID', None)
did = myinf.get('PCI Device ID', None)
vid = myinf.get('PCI Vendor ID', None)
vstr, dstr = get_pci_text_from_ids(sdid, svid, did, vid)
if vstr:
newinf['information']['PCI Vendor'] = vstr
if dstr:
newinf['name'] = dstr
invitems.append(newinf)
def handle_sensors(self):
if self.element[-1] == '':
self.element = self.element[:-1]
@@ -986,6 +1080,16 @@ class IpmiHandler(object):
self.ipmicmd.set_mci(mci)
return
def handle_hostname(self):
if 'read' == self.op:
hostname = self.ipmicmd.get_hostname()
self.output.put(msg.Hostname(self.node, hostname))
return
elif 'update' == self.op:
hostname = self.inputdata.hostname(self.node)
self.ipmicmd.set_hostname(hostname)
return
def handle_domain_name(self):
if 'read' == self.op:
dn = self.ipmicmd.get_domain_name()
@@ -996,10 +1100,17 @@ class IpmiHandler(object):
self.ipmicmd.set_domain_name(dn)
return
def handle_sysconfig(self):
def handle_sysconfigclear(self):
if 'read' == self.op:
raise exc.InvalidArgumentException(
'Cannot read the "clear" resource')
self.ipmicmd.clear_system_configuration()
def handle_sysconfig(self, advanced=False):
if 'read' == self.op:
self.output.put(msg.ConfigSet(
self.node, self.ipmicmd.get_system_configuration()))
self.node, self.ipmicmd.get_system_configuration(
hideadvanced=not advanced)))
elif 'update' == self.op:
self.ipmicmd.set_system_configuration(
self.inputdata.get_attributes(self.node))
@@ -1054,6 +1165,11 @@ class IpmiHandler(object):
self.output.put(msg.License(self.node, available))
return
def handle_description(self):
dsc = self.ipmicmd.get_description()
self.output.put(msg.KeyValueData(dsc, self.node))
def _str_health(health):
if isinstance(health, str):
return health
@@ -1098,6 +1214,9 @@ def retrieve(nodes, element, configmanager, inputdata):
elif '/'.join(element).startswith('media/uploads'):
return firmwaremanager.list_updates(nodes, configmanager.tenant,
element, 'mediaupload')
elif '/'.join(element).startswith('support/servicedata'):
return firmwaremanager.list_updates(nodes, configmanager.tenant,
element, 'ffdc')
else:
return perform_requests('read', nodes, element, configmanager, inputdata)
@@ -1109,5 +1228,8 @@ def delete(nodes, element, configmanager, inputdata):
elif '/'.join(element).startswith('media/uploads'):
return firmwaremanager.remove_updates(nodes, configmanager.tenant,
element, type='mediaupload')
elif '/'.join(element).startswith('support/servicedata'):
return firmwaremanager.remove_updates(nodes, configmanager.tenant,
element, type='ffdc')
return perform_requests(
'delete', nodes, element, configmanager, inputdata)
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -22,9 +22,22 @@
import confluent.exceptions as cexc
import confluent.interface.console as conapi
import confluent.log as log
import cryptography
import eventlet
import hashlib
import sys
sys.modules['gssapi'] = None
paramiko = eventlet.import_patched('paramiko')
warnhostkey = False
if cryptography.__version__.split('.') < ['1', '5']:
# older cryptography with paramiko breaks most key support except
# ed25519
warnhostkey = True
paramiko.transport.Transport._preferred_keys = filter(
lambda x: 'ed25519' in x,
paramiko.transport.Transport._preferred_keys)
class HostKeyHandler(paramiko.client.MissingHostKeyPolicy):
@@ -63,6 +76,7 @@ class SshShell(conapi.Console):
def __init__(self, node, config, username='', password=''):
self.node = node
self.ssh = None
self.datacallback = None
self.nodeconfig = config
self.username = username
self.password = password
@@ -109,6 +123,26 @@ class SshShell(conapi.Console):
self.username = ''
self.password = ''
self.datacallback('\r\nlogin as: ')
return
except cexc.PubkeyInvalid as pi:
self.keyaction = ''
self.candidatefprint = pi.fingerprint
self.datacallback(pi.message)
self.keyattrname = pi.attrname
self.datacallback('\r\nNew fingerprint: ' + pi.fingerprint)
self.inputmode = -1
self.datacallback('\r\nEnter "disconnect" or "accept": ')
return
except paramiko.SSHException as pi:
self.inputmode = -2
warn = str(pi)
if warnhostkey:
warn += ' (Older cryptography package on this host only ' \
'works with ed25519, check ssh startup on target ' \
'and permissions on /etc/ssh/*key)\r\n' \
'Press Enter to close...'
self.datacallback('\r\n' + warn)
return
self.inputmode = 2
self.connected = True
@@ -116,7 +150,36 @@ class SshShell(conapi.Console):
self.rxthread = eventlet.spawn(self.recvdata)
def write(self, data):
if self.inputmode == 0:
if self.inputmode == -2:
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
elif self.inputmode == -1:
while len(data) and data[0] == b'\x7f' and len(self.keyaction):
self.datacallback('\b \b') # erase previously echoed value
self.keyaction = self.keyaction[:-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.keyaction += data
if '\r' in self.keyaction:
action = self.keyaction.split('\r')[0]
if action.lower() == 'accept':
self.nodeconfig.set_node_attributes(
{self.node:
{self.keyattrname: self.candidatefprint}})
self.datacallback('\r\n')
self.logon()
elif action.lower() == 'disconnect':
self.datacallback(conapi.ConsoleEvent.Disconnect)
else:
self.keyaction = ''
self.datacallback('\r\nEnter "disconnect" or "accept": ')
elif len(data) > 0:
self.datacallback(data)
elif 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]
@@ -128,7 +191,7 @@ class SshShell(conapi.Console):
data = data[:delidx - 1] + data[delidx + 1:]
self.username += data
if '\r' in self.username:
self.username, self.password = self.username.split('\r')
self.username, self.password = self.username.split('\r')[:2]
lastdata = data.split('\r')[0]
if lastdata != '':
self.datacallback(lastdata)
@@ -155,6 +218,7 @@ class SshShell(conapi.Console):
def close(self):
if self.ssh is not None:
self.ssh.close()
self.datacallback = None
def create(nodes, element, configmanager, inputdata):
if len(nodes) == 1:
@@ -116,6 +116,7 @@ class ExecConsole(conapi.Console):
break
if self.subproc is not None and self.subproc.poll() is None:
self.subproc.kill()
self._datacallback = None
class Plugin(object):
@@ -30,6 +30,9 @@ class _ShellHandler(consoleserver.ConsoleHandler):
_genwatchattribs = False
_logtobuffer = False
def check_collective(self, attrvalue):
return
def log(self, *args, **kwargs):
# suppress logging through proving a stub 'log' function
return
+13 -4
View File
@@ -92,13 +92,22 @@ class Session(object):
errstr, errnum, erridx, answers = rsp
if errstr:
errstr = str(errstr)
if errstr in ('unknownUserName', 'wrongDigest'):
raise exc.TargetEndpointBadCredentials(errstr)
finerr = errstr + ' while trying to connect to ' \
'{0}'.format(self.server)
if errstr in ('Unknown USM user', 'unknownUserName',
'wrongDigest', 'Wrong SNMP PDU digest'):
raise exc.TargetEndpointBadCredentials(finerr)
# need to do bad credential versus timeout
raise exc.TargetEndpointUnreachable(errstr)
raise exc.TargetEndpointUnreachable(finerr)
elif errnum:
raise exc.ConfluentException(errnum.prettyPrint())
raise exc.ConfluentException(errnum.prettyPrint() +
' while trying to connect to '
'{0}'.format(self.server))
for ans in answers:
if not obj[0].isPrefixOf(ans[0]):
# PySNMP returns leftovers in a bulk command
# filter out such leftovers
break
yield ans
except snmperr.WrongValueError:
raise exc.TargetEndpointBadCredentials('Invalid SNMPv3 password')
+143 -13
View File
@@ -21,6 +21,8 @@
#
import atexit
import ctypes
import ctypes.util
import errno
import os
import pwd
@@ -29,6 +31,7 @@ import struct
import sys
import traceback
import eventlet.green.select as select
import eventlet.green.socket as socket
import eventlet.green.ssl as ssl
import eventlet
@@ -41,7 +44,8 @@ import confluent.exceptions as exc
import confluent.log as log
import confluent.core as pluginapi
import confluent.shellserver as shellserver
import confluent.collective.manager as collective
import confluent.util as util
tracelog = None
auditlog = None
@@ -54,6 +58,22 @@ except AttributeError:
else:
SO_PEERCRED = 17
try:
# Python core TLS despite improvements still has no support for custom
# verify functions.... try to use PyOpenSSL where available to support
# client certificates with custom verify
import eventlet.green.OpenSSL.SSL as libssl
# further, not even pyopenssl exposes SSL_CTX_set_cert_verify_callback
# so we need to ffi that in using a strategy compatible with PyOpenSSL
import OpenSSL.SSL as libssln
import OpenSSL.crypto as crypto
from OpenSSL._util import ffi
except ImportError:
libssl = None
ffi = None
crypto = None
plainsocket = None
class ClientConsole(object):
def __init__(self, client):
@@ -82,7 +102,7 @@ def send_data(connection, data):
raise
def sessionhdl(connection, authname, skipauth=False):
def sessionhdl(connection, authname, skipauth=False, cert=None):
# For now, trying to test the console stuff, so let's just do n4.
authenticated = False
authdata = None
@@ -99,6 +119,15 @@ def sessionhdl(connection, authname, skipauth=False):
while not authenticated: # prompt for name and passphrase
send_data(connection, {'authpassed': 0})
response = tlvdata.recv(connection)
if 'collective' in response:
return collective.handle_connection(connection, cert,
response['collective'])
if 'dispatch' in response:
dreq = tlvdata.recvall(connection, response['dispatch']['length'])
return pluginapi.handle_dispatch(connection, cert, dreq,
response['dispatch']['name'])
if 'proxyconsole' in response:
return start_proxy_term(connection, cert, response['proxyconsole'])
authname = response['username']
passphrase = response['password']
# note(jbjohnso): here, we need to authenticate, but not
@@ -114,6 +143,18 @@ def sessionhdl(connection, authname, skipauth=False):
cfm = authdata[1]
send_data(connection, {'authpassed': 1})
request = tlvdata.recv(connection)
if 'collective' in request and skipauth:
if not libssl:
tlvdata.send(
connection,
{'collective': {'error': 'Server either does not have '
'python-pyopenssl installed or has an '
'incorrect version installed '
'(e.g. pyOpenSSL would need to be '
'replaced with python-pyopenssl)'}})
return
return collective.handle_connection(connection, None, request['collective'],
local=True)
while request is not None:
try:
process_request(
@@ -172,7 +213,7 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
if operation == 'start':
return start_term(authname, cfm, connection, params, path,
authdata, skipauth)
elif operation == 'shutdown':
elif operation == 'shutdown' and skipauth:
configmanager.ConfigManager.shutdown()
else:
hdlr = pluginapi.handle_path(path, operation, cfm, params)
@@ -187,6 +228,19 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
send_response(hdlr, connection)
return
def start_proxy_term(connection, cert, request):
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
droneinfo = configmanager.get_collective_member(request['name'])
if not util.cert_matches(droneinfo['fingerprint'], cert):
connection.close()
return
cfm = configmanager.ConfigManager(request['tenant'])
ccons = ClientConsole(connection)
consession = consoleserver.ConsoleSession(
node=request['node'], configmanager=cfm, username=request['user'],
datacallback=ccons.sendall, skipreplay=request['skipreplay'],
direct=False)
term_interact(None, None, ccons, None, connection, consession, None)
def start_term(authname, cfm, connection, params, path, authdata, skipauth):
elems = path.split('/')
@@ -210,12 +264,16 @@ def start_term(authname, cfm, connection, params, path, authdata, skipauth):
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")
term_interact(authdata, authname, ccons, cfm, connection, consession,
skipauth)
def term_interact(authdata, authname, ccons, cfm, connection, consession,
skipauth):
send_data(connection, {'started': 1})
ccons.startsending()
bufferage = consession.get_buffer_age()
@@ -226,7 +284,7 @@ def start_term(authname, cfm, connection, params, path, authdata, skipauth):
if type(data) == dict:
if data['operation'] == 'stop':
consession.destroy()
return
break
elif data['operation'] == 'break':
consession.send_break()
continue
@@ -247,11 +305,12 @@ def start_term(authname, cfm, connection, params, path, authdata, skipauth):
continue
if not data:
consession.destroy()
return
break
consession.write(data)
def _tlshandler(bind_host, bind_port):
global plainsocket
plainsocket = socket.socket(socket.AF_INET6)
plainsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
plainsocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@@ -271,13 +330,54 @@ def _tlshandler(bind_host, bind_port):
eventlet.spawn_n(_tlsstartup, cnn)
if ffi:
@ffi.callback("int(*)( X509_STORE_CTX *, void*)")
def verify_stub(store, misc):
return 1
def _tlsstartup(cnn):
authname = None
cnn = ssl.wrap_socket(cnn, keyfile="/etc/confluent/privkey.pem",
certfile="/etc/confluent/srvcert.pem",
ssl_version=ssl.PROTOCOL_TLSv1,
server_side=True)
sessionhdl(cnn, authname)
cert = None
if libssl:
# most fully featured SSL function
ctx = libssl.Context(libssl.SSLv23_METHOD)
ctx.set_options(libssl.OP_NO_SSLv2 | libssl.OP_NO_SSLv3 |
libssl.OP_NO_TLSv1 | libssl.OP_NO_TLSv1_1 |
libssl.OP_CIPHER_SERVER_PREFERENCE)
ctx.set_cipher_list(
'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:'
'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384')
ctx.set_tmp_ecdh(crypto.get_elliptic_curve('secp384r1'))
ctx.use_certificate_file('/etc/confluent/srvcert.pem')
ctx.use_privatekey_file('/etc/confluent/privkey.pem')
ctx.set_verify(libssln.VERIFY_PEER, lambda *args: True)
libssln._lib.SSL_CTX_set_cert_verify_callback(ctx._context,
verify_stub, ffi.NULL)
cnn = libssl.Connection(ctx, cnn)
cnn.set_accept_state()
cnn.do_handshake()
cert = cnn.get_peer_certificate()
else:
try:
# Try relatively newer python TLS function
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
ctx.options |= ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
ctx.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
ctx.options |= ssl.OP_CIPHER_SERVER_PREFERENCE
ctx.set_ciphers(
'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:'
'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384')
ctx.load_cert_chain('/etc/confluent/srvcert.pem',
'/etc/confluent/privkey.pem')
cnn = ctx.wrap_socket(cnn, server_side=True)
except AttributeError:
# Python 2.6 era, go with best effort
cnn = ssl.wrap_socket(cnn, keyfile="/etc/confluent/privkey.pem",
certfile="/etc/confluent/srvcert.pem",
ssl_version=ssl.PROTOCOL_TLSv1,
server_side=True)
sessionhdl(cnn, authname, cert=cert)
def removesocket():
try:
@@ -336,6 +436,36 @@ class SockApi(object):
global tracelog
tracelog = log.Logger('trace')
auditlog = log.Logger('audit')
self.tlsserver = None
if self.should_run_remoteapi():
self.start_remoteapi()
else:
eventlet.spawn_n(self.watch_for_cert)
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
def watch_for_cert(self):
libc = ctypes.CDLL(ctypes.util.find_library('c'))
watcher = libc.inotify_init()
if libc.inotify_add_watch(watcher, '/etc/confluent/', 0x100) > -1:
while True:
select.select((watcher,), (), (), 86400)
if self.should_run_remoteapi():
os.close(watcher)
self.start_remoteapi()
break
def should_run_remoteapi(self):
return os.path.exists("/etc/confluent/srvcert.pem")
def stop_remoteapi(self):
if self.tlsserver is None:
return
self.tlsserver.kill()
plainsocket.close()
self.tlsserver = None
def start_remoteapi(self):
if self.tlsserver is not None:
return
self.tlsserver = eventlet.spawn(
_tlshandler, self.bind_host, self.bind_port)
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
+15
View File
@@ -24,6 +24,7 @@ import netifaces
import os
import re
import socket
import ssl
import struct
@@ -99,6 +100,20 @@ def monotonic_time():
return os.times()[4]
def get_certificate_from_file(certfile):
cert = open(certfile, 'rb').read()
inpemcert = False
prunedcert = ''
for line in cert.split('\n'):
if '-----BEGIN CERTIFICATE-----' in line:
inpemcert = True
if inpemcert:
prunedcert += line
if '-----END CERTIFICATE-----' in line:
break
return ssl.PEM_cert_to_DER_cert(prunedcert)
def get_fingerprint(certificate, algo='sha512'):
if algo == 'sha256':
return 'sha256$' + hashlib.sha256(certificate).hexdigest()
+1 -1
View File
@@ -12,7 +12,7 @@ Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dns, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-pyte, python-lxml, python-eficompressor
Requires: python-pyghmi >= 1.0.34, python-eventlet, python-greenlet, python-pycryptodomex >= 3.4.7, confluent_client, python-pyparsing, python-paramiko, python-dns, python-netifaces, python2-pyasn1 >= 0.2.3, python-pysnmp >= 4.3.4, python-pyte, python-lxml, python-eficompressor, python-setuptools
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
Url: http://xcat.sf.net/
+3 -1
View File
@@ -6,4 +6,6 @@ if [ "$NUMCOMMITS" != "$VERSION" ]; then
fi
echo $VERSION > VERSION
sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py
echo '__version__ = "'$VERSION'"' > confluent/__init__.py
if [ -f confluent/client.py ]; then
echo '__version__ = "'$VERSION'"' > confluent/__init__.py
fi
-22
View File
@@ -1,22 +0,0 @@
[metadata]
name = confluent_server
summary = Confluent systems management daemon
description-file =
README.txt
author = Jarrod Johnson
author-email = jbjohnso@us.ibm.com
home-page = http://xcat.sf.net/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
[files]
packages =
confluent
confluent/interface
confluent/config
+4 -2
View File
@@ -15,10 +15,12 @@ setup(
'confluent/networking/',
'confluent/plugins/hardwaremanagement/',
'confluent/plugins/shell/',
'confluent/collective/',
'confluent/plugins/configuration/'],
install_requires=['paramiko', 'pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
'pyghmi>=0.6.5'],
scripts=['bin/confluent', 'bin/confluentdbutil'],
'dnspython', 'netifaces', 'pyte', 'pysnmp', 'pyparsing',
'pyghmi>=1.0.44'],
scripts=['bin/confluent', 'bin/confluentdbutil', 'bin/collective'],
data_files=[('/etc/init.d', ['sysvinit/confluent']),
('/usr/lib/sysctl.d', ['sysctl/confluent.conf']),
('/usr/lib/systemd/system', ['systemd/confluent.service']),
+1 -1
View File
@@ -4,7 +4,7 @@ Description=Confluent hardware manager
[Service]
Type=forking
PIDFile=/var/run/confluent/pid
#PIDFile=/var/run/confluent/pid
ExecStart=/opt/confluent/bin/confluent
ExecStop=/opt/confluent/bin/confetty shutdown /
Restart=on-failure
-38
View File
@@ -1,38 +0,0 @@
function getRequest(url, success) {
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.setRequestHeader('Accept', 'application/json');
request.onload = function() {
if (this.status >= 200 && this.status <= 400) {
success(JSON.parse(this.responseText));
}
};
request.send();
}
document.addEventListener('DOMContentLoaded', function() {
getRequest("/confluent-api/nodes/", function( data) {
var items = [];
var options = [];
var nodename = "";
data["_links"]["item"].forEach( function( val, key ) {
console.log(val);
if (typeof(val) == "object") {
nodename = val.href;
} else {
nodename = val;
}
console.log(nodename);
nodename = nodename.replace('/', '');
var myrow = document.createElement('div');
myrow.innerHTML = "<button id="+nodename+">"+nodename+"</button><br>";
document.getElementById("nodes").appendChild(myrow);
document.getElementById(nodename).addEventListener("click", function( event ) {
var tname = this.id;
var url = "/confluent-api/nodes/" + tname + "/console/session";
new ConsoleWindow(url, tname);
});
});
});
}); // end document
-742
View File
@@ -1,742 +0,0 @@
/**
* tty.js
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* Copyright 2014, IBM Corporation
* Copyright 2014, Lenovo
*/
;(function() {
'use strict';
/**
* Elements
*/
var document = this.document
, window = this
, root
, body
, h1
, open
, lights;
/**
* Helpers
*/
var EventEmitter = Terminal.EventEmitter
, inherits = Terminal.inherits
, on = Terminal.on
, off = Terminal.off
, cancel = Terminal.cancel;
function postRequest(url, data, success) {
var request = new XMLHttpRequest();
request.open('POST', url, true);
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('Accept', 'application/json');
request.onload = function() {
if (this.status >= 200 && this.status < 400) {
success(JSON.parse(this.responseText));
}
};
if (data) {
request.send(JSON.stringify(data));
} else {
request.send("");
}
request = null;
}
/**
* Console
*/
function ConsoleWindow(consoleurl, nodename) {
var self = this;
if (!(this instanceof ConsoleWindow)) {
return new ConsoleWindow(consoleurl, nodename);
}
EventEmitter.call(this);
var el
, grip
, bar
, button
, title;
el = document.createElement('div');
el.className = 'window';
grip = document.createElement('div');
grip.className = 'grip';
bar = document.createElement('div');
bar.className = 'bar';
button = document.createElement('div');
button.innerHTML = 'x';
button.title = 'close';
button.className = 'tab';
title = document.createElement('div');
title.className = 'title';
title.innerHTML = nodename;
this.nodename = nodename;
this.element = el;
this.grip = grip;
this.bar = bar;
this.button = button;
this.title = title;
this.consoleurl = consoleurl;
this.tabs = [];
this.focused = null;
this.cols = 100; //Terminal.geometry[0];
this.rows = 30; //Terminal.geometry[1];
el.appendChild(grip);
el.appendChild(bar);
bar.appendChild(title);
bar.appendChild(button);
document.body.appendChild(el);
//tty.windows.push(this);
this.createTab();
this.focus();
this.bind();
this.tabs[0].once('open', function() {
//tty.emit('open window', self);
self.emit('open');
});
}
inherits(ConsoleWindow, EventEmitter);
ConsoleWindow.prototype.bind = function() {
var self = this
, el = this.element
, bar = this.bar
, grip = this.grip
, button = this.button
, last = 0;
on(button, 'click', function(ev) {
self.destroy();
return cancel(ev);
});
on(grip, 'mousedown', function(ev) {
self.focus();
self.resizing(ev);
return cancel(ev);
});
on(el, 'mousedown', function(ev) {
if (ev.target !== el && ev.target !== bar) return;
self.focus();
cancel(ev);
if (new Date - last < 600) {
return self.maximize();
}
last = new Date;
self.drag(ev);
return cancel(ev);
});
};
ConsoleWindow.prototype.focus = function() {
// Restack
var parent = this.element.parentNode;
if (parent) {
parent.removeChild(this.element);
parent.appendChild(this.element);
}
// Focus Foreground Tab
this.focused.focus();
//tty.emit('focus window', this);
this.emit('focus');
};
ConsoleWindow.prototype.destroy = function() {
if (this.destroyed) return;
this.destroyed = true;
if (this.minimize) this.minimize();
//splice(tty.windows, this);
//if (tty.windows.length) tty.windows[0].focus();
this.element.parentNode.removeChild(this.element);
this.each(function(term) {
term.destroy();
});
//tty.emit('close window', this);
this.emit('close');
};
ConsoleWindow.prototype.drag = function(ev) {
var self = this
, el = this.element;
if (this.minimize) return;
var drag = {
left: el.offsetLeft,
top: el.offsetTop,
pageX: ev.pageX,
pageY: ev.pageY
};
el.style.opacity = '0.60';
el.style.cursor = 'move';
document.documentElement.style.cursor = 'move';
function move(ev) {
el.style.left =
(drag.left + ev.pageX - drag.pageX) + 'px';
var tmptop = (drag.top + ev.pageY - drag.pageY);
if (tmptop < 0) {
tmptop = 0;
}
el.style.top = tmptop + 'px';
}
function up() {
el.style.opacity = '';
el.style.cursor = '';
document.documentElement.style.cursor = '';
off(document, 'mousemove', move);
off(document, 'mouseup', up);
var ev = {
left: el.style.left.replace(/\w+/g, ''),
top: el.style.top.replace(/\w+/g, '')
};
//tty.emit('drag window', self, ev);
self.emit('drag', ev);
}
on(document, 'mousemove', move);
on(document, 'mouseup', up);
};
ConsoleWindow.prototype.resizing = function(ev) {
var self = this
, el = this.element
, term = this.focused;
if (this.minimize) delete this.minimize;
var resize = {
w: el.clientWidth,
h: el.clientHeight
};
el.style.overflow = 'hidden';
el.style.opacity = '0.70';
el.style.cursor = 'se-resize';
document.documentElement.style.cursor = 'se-resize';
term.element.style.height = '100%';
function move(ev) {
var x, y;
y = el.offsetHeight - term.element.clientHeight;
x = ev.pageX - el.offsetLeft;
y = (ev.pageY - el.offsetTop) - y;
el.style.width = x + 'px';
el.style.height = y + 'px';
}
function up() {
var x, y;
x = el.clientWidth / resize.w;
y = el.clientHeight / resize.h;
x = (x * term.cols) | 0;
y = (y * term.rows) | 0;
self.resize(x, y);
el.style.width = '';
el.style.height = '';
el.style.overflow = '';
el.style.opacity = '';
el.style.cursor = '';
document.documentElement.style.cursor = '';
term.element.style.height = '';
off(document, 'mousemove', move);
off(document, 'mouseup', up);
}
on(document, 'mousemove', move);
on(document, 'mouseup', up);
};
ConsoleWindow.prototype.maximize = function() {
if (this.minimize) return this.minimize();
var self = this
, el = this.element
, term = this.focused
, x
, y;
var m = {
cols: term.cols,
rows: term.rows,
left: el.offsetLeft,
top: el.offsetTop,
root: root.className
};
this.minimize = function() {
delete this.minimize;
el.style.left = m.left + 'px';
el.style.top = m.top + 'px';
el.style.width = '';
el.style.height = '';
term.element.style.width = '';
term.element.style.height = '';
el.style.boxSizing = '';
self.grip.style.display = '';
root.className = m.root;
self.resize(m.cols, m.rows);
//tty.emit('minimize window', self);
self.emit('minimize');
};
window.scrollTo(0, 0);
x = root.clientWidth / term.element.offsetWidth;
y = root.clientHeight / term.element.offsetHeight;
x = (x * term.cols) | 0;
y = (y * term.rows) | 0;
el.style.left = '0px';
el.style.top = '0px';
el.style.width = '100%';
el.style.height = '100%';
term.element.style.width = '100%';
term.element.style.height = '100%';
el.style.boxSizing = 'border-box';
this.grip.style.display = 'none';
root.className = 'maximized';
this.resize(x, y);
//tty.emit('maximize window', this);
this.emit('maximize');
};
ConsoleWindow.prototype.resize = function(cols, rows) {
this.cols = cols;
this.rows = rows;
this.each(function(term) {
term.resize(cols, rows);
});
//tty.emit('resize window', this, cols, rows);
this.emit('resize', cols, rows);
};
ConsoleWindow.prototype.each = function(func) {
var i = this.tabs.length;
while (i--) {
func(this.tabs[i], i);
}
};
ConsoleWindow.prototype.createTab = function() {
return new Tab(this, this.consoleurl);
};
ConsoleWindow.prototype.highlight = function() {
var self = this;
this.element.style.borderColor = 'orange';
setTimeout(function() {
self.element.style.borderColor = '';
}, 200);
this.focus();
};
ConsoleWindow.prototype.focusTab = function(next) {
var tabs = this.tabs
, i = indexOf(tabs, this.focused)
, l = tabs.length;
if (!next) {
if (tabs[--i]) return tabs[i].focus();
if (tabs[--l]) return tabs[l].focus();
} else {
if (tabs[++i]) return tabs[i].focus();
if (tabs[0]) return tabs[0].focus();
}
return this.focused && this.focused.focus();
};
ConsoleWindow.prototype.nextTab = function() {
return this.focusTab(true);
};
ConsoleWindow.prototype.previousTab = function() {
return this.focusTab(false);
};
/**
* Tab
*/
function Tab(win, consoleurl) {
var self = this;
var cols = win.cols
, rows = win.rows;
Terminal.call(this, {
cols: cols,
rows: rows
});
var button = document.createElement('div');
button.className = 'tab';
button.innerHTML = '\u2022';
//win.bar.appendChild(button);
on(button, 'click', function(ev) {
if (ev.ctrlKey || ev.altKey || ev.metaKey || ev.shiftKey) {
self.destroy();
} else {
self.focus();
}
return cancel(ev);
});
this.id = '';
this.consoleurl = consoleurl;
this.clientcount = 0;
this.connectstate = 'unconnected';
this.lasterror = ''
this.window = win;
this.button = button;
this.element = null;
this.process = '';
this.open();
this.hookKeys();
// Now begins the code that will embarass me when I actually know my way
// around javascript -jbjohnso
this.sessid = '';
this.datapending = false;
this.waitingdata = false;
this.sentdata = function(data, textStatus, jqXHR) {
if (this.waitingdata) {
postRequest(consoleurl, { session: this.sessid, bytes: this.waitingdata }, this.sentdata);
this.waitingdata = false;
} else {
this.datapending = false;
}
}.bind(this);
this.on('data', function(data) {
// Send data to console from terminal
if (this.datapending) {
if (!this.waitingdata) {
this.waitingdata = data;
} else {
this.waitingdata = this.waitingdata + data;
}
return;
}
this.datapending = true;
postRequest(consoleurl, { session: this.sessid, bytes: data }, this.sentdata);
}.bind(this));
this.gotdata = function(data, textStatus, jqXHR) {
if ("data" in data) {
this.write(data.data);
}
var updatetitle = false;
var updateinfo = [];
if ("connectstate" in data) {
updatetitle = true;
this.connectstate = data.connectstate;
}
if (this.connectstate != "connected") {
updateinfo.push(this.connectstate);
} else {
self.lasterror = '';
}
if ("error" in data) {
updatetitle = true;
this.lasterror = data.error
}
if (this.lasterror != '') {
updateinfo.push(this.lasterror);
}
if ("clientcount" in data) {
updatetitle = true;
this.clientcount = data.clientcount;
}
if (this.clientcount > 1) {
updateinfo.push("clients: " + this.clientcount.toString());
}
if (updatetitle == true) {
if (updateinfo.length > 0) {
this.window.title.innerHTML = this.window.nodename + " [" + updateinfo.join() + "]";
} else {
this.window.title.innerHTML = this.window.nodename;
}
}
postRequest(consoleurl, { session: this.sessid }, this.gotdata);
}.bind(this);
this.gotsession = function(data, textStatus, jqXHR) {
this.sessid = data.session
postRequest(consoleurl, { session: this.sessid }, this.gotdata);
}.bind(this);
postRequest(consoleurl, false, this.gotsession);
win.tabs.push(this);
};
inherits(Tab, Terminal);
Tab.prototype._write = Tab.prototype.write;
Tab.prototype.write = function(data) {
if (this.window.focused !== this) this.button.style.color = 'red';
return this._write(data);
};
Tab.prototype._focus = Tab.prototype.focus;
Tab.prototype.focus = function() {
if (Terminal.focus === this) return;
var win = this.window;
// maybe move to Tab.prototype.switch
if (win.focused !== this) {
if (win.focused) {
if (win.focused.element.parentNode) {
win.focused.element.parentNode.removeChild(win.focused.element);
}
win.focused.button.style.fontWeight = '';
}
win.element.appendChild(this.element);
win.focused = this;
//win.title.innerHTML = this.process;
this.button.style.fontWeight = 'bold';
this.button.style.color = '';
}
this._focus();
win.focus();
//tty.emit('focus tab', this);
this.emit('focus');
};
Tab.prototype._resize = Tab.prototype.resize;
Tab.prototype.resize = function(cols, rows) {
//this.socket.emit('resize', this.id, cols, rows);
this._resize(cols, rows);
//tty.emit('resize tab', this, cols, rows);
this.emit('resize', cols, rows);
};
Tab.prototype.__destroy = Tab.prototype.destroy;
Tab.prototype._destroy = function() {
if (this.destroyed) return;
this.destroyed = true;
var win = this.window;
this.button.parentNode.removeChild(this.button);
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
if (tty.terms[this.id]) delete tty.terms[this.id];
splice(win.tabs, this);
if (win.focused === this) {
win.previousTab();
}
if (!win.tabs.length) {
win.destroy();
}
this.__destroy();
};
Tab.prototype.destroy = function() {
if (this.destroyed) return;
//TODO: politely let server know of client closure
this._destroy();
//tty.emit('close tab', this);
this.emit('close');
};
Tab.prototype.hookKeys = function() {
var self = this;
// Alt-[jk] to quickly swap between windows.
this.on('key', function(key, ev) {
if (Terminal.focusKeys === false) {
return;
}
var offset
, i;
if (key === '\x1bj') {
offset = -1;
} else if (key === '\x1bk') {
offset = +1;
} else {
return;
}
i = indexOf(tty.windows, this.window) + offset;
this._ignoreNext();
if (tty.windows[i]) return tty.windows[i].highlight();
if (offset > 0) {
if (tty.windows[0]) return tty.windows[0].highlight();
} else {
i = tty.windows.length - 1;
if (tty.windows[i]) return tty.windows[i].highlight();
}
return this.window.highlight();
});
this.on('request paste', function(key) {
this.socket.emit('request paste', function(err, text) {
if (err) return;
self.send(text);
});
});
this.on('request create', function() {
this.window.createTab();
});
this.on('request term', function(key) {
if (this.window.tabs[key]) {
this.window.tabs[key].focus();
}
});
this.on('request term next', function(key) {
this.window.nextTab();
});
this.on('request term previous', function(key) {
this.window.previousTab();
});
};
Tab.prototype._ignoreNext = function() {
// Don't send the next key.
var handler = this.handler;
this.handler = function() {
this.handler = handler;
};
var showCursor = this.showCursor;
this.showCursor = function() {
this.showCursor = showCursor;
};
};
/**
* Program-specific Features
*/
Tab.prototype._bindMouse = Tab.prototype.bindMouse;
Tab.prototype.bindMouse = function() {
if (!Terminal.programFeatures) return this._bindMouse();
var self = this;
var wheelEvent = 'onmousewheel' in window
? 'mousewheel'
: 'DOMMouseScroll';
on(self.element, wheelEvent, function(ev) {
if (self.mouseEvents) return;
if ((ev.type === 'mousewheel' && ev.wheelDeltaY > 0)
|| (ev.type === 'DOMMouseScroll' && ev.detail < 0)) {
// page up
self.keyDown({keyCode: 33});
} else {
// page down
self.keyDown({keyCode: 34});
}
return cancel(ev);
});
return this._bindMouse();
};
/**
* Helpers
*/
function indexOf(obj, el) {
var i = obj.length;
while (i--) {
if (obj[i] === el) return i;
}
return -1;
}
function splice(obj, el) {
var i = indexOf(obj, el);
if (~i) obj.splice(i, 1);
}
function sanitize(text) {
if (!text) return '';
return (text + '').replace(/[&<>]/g, '')
}
this.ConsoleWindow = ConsoleWindow;
}).call(function() {
return this;
}());
File diff suppressed because it is too large Load Diff