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

Compare commits

..

669 Commits

Author SHA1 Message Date
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 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
Jarrod Johnson 308db99dbb Fix inconsistent dict member extension
If two portions of a list come back piecewise from the plugin that
are both lists, extend them rather than making a nested list.
2018-03-15 12:09:45 -04:00
Jarrod Johnson a20b0abb43 Do not clear the buffer on superfluous reopen
If someone does a reopen, try to preserve the buffer, unless connect
proves there to be a deeper issue.  The risk of staleness is low, but
the experience of the whole screen clearing is tricky.  This was not
such an issue at the time, but using pyte causes clearbuffer to also
clear connected client terminals.
2018-03-14 17:00:44 -04:00
Jarrod Johnson 7413c44df8 Fix manual discovery
In manual discovery, maccount is not a field in the info, as no macmap
processing is done in manual.
2018-03-14 09:27:29 -04:00
Jarrod Johnson 463f61fac7 Modify XSS-Protection directive 2018-03-12 13:41:18 -04:00
Jarrod Johnson 0f60fc6df7 Fix uninitialized self._prevdict
self._prevdict was referenced without initialization.
2018-03-07 10:21:35 -05:00
Jarrod Johnson 110820e7b7 Revert "Accommodate XCC firmware behavior"
This reverts commit 9baa1f5652.
2018-03-06 15:52:53 -05:00
Jarrod Johnson 71214eb613 Revert "Correct indentation"
This reverts commit a2163244db.
2018-03-06 15:52:45 -05:00
Jarrod Johnson 889eda3d96 Merge remote-tracking branch 'upstream/master' 2018-03-06 11:25:54 -05:00
Jarrod Johnson 7593d21a87 Add missing exceptions import
exc was not imported
2018-03-06 11:25:36 -05:00
Jarrod Johnson 972801d41f Merge pull request #95 from aduffy19/nodepowerUpdate
Add previous option to nodepower command
2018-03-05 15:44:28 -05:00
Amanda Duffy b49531dfa5 Add previous option to nodepower command 2018-03-05 15:41:28 -05:00
Jarrod Johnson 3ae7d85820 Fix confluentdbgcli on screen resize 2018-03-01 13:39:25 -05:00
Jarrod Johnson 0d06eedc81 Move curruuid up so it is always set
If detected pre-discovered, curruuid was not being set correctly
2018-02-28 11:53:03 -05:00
Jarrod Johnson ca27385eaa Fix confluentdbutil restore with typed attributes
Typed attributes would fail the check.  Additionally, the error itself
was buggy, so fix that while we are at it.
2018-02-27 14:51:37 -05:00
Jarrod Johnson 9269c9feff Provide more reasonable error message to user
If a protocol is unsupported, propogate error message usefully.
2018-02-27 14:51:00 -05:00
Jarrod Johnson 7736056bf2 Fix hangtraces analyzer
With threadid, the analyzer was failing to collapse identical threads.
2018-02-27 11:28:37 -05:00
Jarrod Johnson 74e34f874c Merge remote-tracking branch 'upstream/master' 2018-02-26 15:49:54 -05:00
Jarrod Johnson 530c6553fd Block creation of nodes that can't be in noderanges
Noderange grammar prevents referencing certain nodenames.  Use the
noderange grammar to evaluate any incoming node or group at a very
low level.
2018-02-26 15:47:47 -05:00
Jarrod Johnson 2e3cd53ded Merge pull request #92 from aduffy19/nodeshellMan
Add [options] and -c description to nodeshell and nodemedia man
2018-02-23 15:46:00 -05:00
Amanda Duffy 4cc80f6c40 Add [options] and -c description to nodeshell and nodemedia man 2018-02-23 14:26:40 -05:00
Jarrod Johnson ff0ab4086a Merge pull request #91 from andywray/patch-2
Update nodemedia man page
2018-02-22 16:36:47 -05:00
andywray 337e8d6337 Update nodemedia man page
Fix bolding
2018-02-22 16:36:09 -05:00
Jarrod Johnson 1dd922c262 Merge pull request #90 from andywray/patch-1
Update nodemedia man page
2018-02-22 16:20:05 -05:00
andywray dae6f74acc Update nodemedia man page
Add (insecure) text
2018-02-22 16:19:21 -05:00
Jarrod Johnson 2323afc093 Merge pull request #89 from andywray/master
Fix nodemedia man page
2018-02-22 16:05:34 -05:00
andywray 9c9dffc385 Fix nodemedia man page
Put newlines in examples
2018-02-22 16:03:41 -05:00
Jarrod Johnson 62ac582b61 Provide better reseat error message on non-enclosure
If a user were to try to reseat a node that isn't enclosure based or
at least does not have the configuration, provide a clue as to what
happened.
2018-02-22 14:56:25 -05:00
Jarrod Johnson d473d23725 Add ability to specify attribs from environment
This is of particular use for passing passwords safely
into nodeattrib/nodegroupattrib.
2018-02-22 09:55:46 -05:00
Jarrod Johnson c5ce302717 Add usage back to nodefirmware
If the arguments are fruitless, provide a usage message rather than
blank.
2018-02-20 10:17:02 -05:00
Jarrod Johnson 22d79598b3 Fix erroneous parameter parsing
The change to add the selective nodefirmware queries incorrectly
mangled update.
2018-02-20 10:10:25 -05:00
Jarrod Johnson d7b3859460 Try to have collate preserve original noderange
When a noderange is given, prepare for a reverse lookup of same nodes,
so collate has a shot of giving back what was put in.
2018-02-14 15:09:21 -05:00
Jarrod Johnson 34f03cb217 Update nodefirmware man page 2018-02-14 14:56:45 -05:00
Jarrod Johnson 0fd2d26f82 Add access to nodefirmware shortcut
The nodefirmware can now request firmware more selectively.
2018-02-14 14:38:20 -05:00
Jarrod Johnson 453f6d8016 Catch general SSL errors to BMC
Provide better info on general SSL misbehaviors.
2018-02-13 13:17:29 -05:00
Jarrod Johnson ee84622e7d Dynamic retry timer
Opportunistically try to be more aggressive, scaling back
with retry number and cluster size.
2018-02-12 14:34:12 -05:00
Jarrod Johnson 2bfa890faf Actually enable ^=
It was inadvertently omitted.
2018-02-08 19:50:19 -05:00
Jarrod Johnson 94bce2f65b Unofficially tolerate pasu and own input
If someone tries to use this with pasu syntax, try to do the right
thing.  Similarly, if trying to paste the same syntax nodeconfig
uses on output, tolerate that as well.
2018-02-08 19:39:39 -05:00
Jarrod Johnson cc16aed27a Add ,= and ^= to group management
This greatly simplifies a common group membership task.
2018-02-08 19:30:06 -05:00
Jarrod Johnson 414572f626 Fix nodeattrib
nodeattrib was broken by the nodeconfig changes
2018-02-08 13:45:17 -05:00
Jarrod Johnson bac37dfa8d Update man page content
Provide documentation on the nodeconfig command, and fix a typo
2018-02-08 11:32:05 -05:00
Jarrod Johnson d3783aaa98 Change to shorter 'node' example
node or nodename are now acceptable, and {node} is less clumsy.
2018-02-08 11:18:48 -05:00
Jarrod Johnson 7dbdf2a6aa Add ability to exclude settings
This permits nodeconfig to prune out less interesting pieces of data
2018-02-07 15:46:08 -05:00
Jarrod Johnson d7322f013b Correct typo 2018-02-06 17:00:45 -05:00
Jarrod Johnson 024d37d633 Add support to show settings detail in nodeconfig
settings have lots of text, provide access through '-d' argument
2018-02-06 16:52:13 -05:00
Jarrod Johnson 8ef90a457a Add comparedefault to nodeconfig command
This allows gathering relevant settings that differ from default
2018-02-06 16:31:20 -05:00
Jarrod Johnson bd5d8c85d8 Add dependencies for the nodeconfig command to work 2018-02-06 08:59:06 -05:00
Jarrod Johnson 0afd9beeac Fix nodeconfig error handling
Additionally, make more strong effort to sort the data.
2018-02-05 15:23:13 -05:00
Jarrod Johnson b37ef7e90c Implement nodeconfig access to system settings
First draft
2018-02-02 17:17:02 -05:00
Jarrod Johnson 685f5c6803 Add system configuration
This gives access to system configuration where available
2018-02-02 16:20:13 -05:00
Jarrod Johnson 7f3763f9eb Clean up media error handling 2018-01-30 15:18:20 -05:00
Jarrod Johnson 290ccecfb9 Exit if requested upload file does not exist
It was fruitlessly sending to server, when it could detect the problem
up front without bothering the server.
2018-01-29 14:36:44 -05:00
Jarrod Johnson 0431e42452 Do not attempt to list after error
On upload/attach errors, do not bother to try to follow up with
list media, as the previous failure is more useful.
2018-01-29 14:21:38 -05:00
Jarrod Johnson 36e68ca852 Fix nodemedia list when errors are present
It would assume that databynode is always good news, but error data
can come as well.
2018-01-29 14:19:25 -05:00
Jarrod Johnson ba0e03d454 Provide a catchall for general certificate mismatch
Beyond the special partial cases for firmware/hardware inventory,
provide a catchall for invalid public keys and a message for it.
2018-01-29 14:17:01 -05:00
Jarrod Johnson e193e92fbf Make TLS errors in hardware/firmware more consistent
Also, place the error near the end where it is more likely to be
noticed, and close to where user would have expected the data to be.
2018-01-29 14:12:34 -05:00
Jarrod Johnson a7b0a449d6 Put ipmi errors in trace log
Put an end to the 'see stderr log'
2018-01-29 13:21:18 -05:00
Jarrod Johnson 2e7a8bee18 Fix nodemedia silent errors
For 'databynode' style responses, error data was suppressed.

Make printerror more thorough at handling such errors.
2018-01-29 11:21:46 -05:00
Jarrod Johnson 9e467c5e57 Add check for invalid dotted attribute names in expressions
This provides better feedback for typos and mistakes.
2018-01-26 17:00:03 -05:00
Jarrod Johnson 68d469788d Fix use of multi dot attributes in expressions
It was formerly only assuming ability to bisect, now recurse to get the
string out of nested attributes.
2018-01-26 14:29:22 -05:00
Jarrod Johnson 7ea99ecbf5 Improve expression error handling
First, refactor the attrname translation to be in a single method.

Recognize if 'None' comes back rather than a dictionary with na 'value'

Add a proper ValueError if an invalid attribute name is used.

Properly catch and transform ValueErrors in the API
2018-01-26 14:16:23 -05:00
Jarrod Johnson 49a444959d Add line wrap to usage error
It mixed output with next prompt, which is ugly
2018-01-24 13:42:13 -05:00
Jarrod Johnson 2628683881 Require minimum pyghmi
Many fixes we rely upon are only found in recent pyghmi.
2018-01-24 13:24:44 -05:00
Jarrod Johnson 1cf472470e Add warning if someone tries attach with a file
Rather than bother the server, sort out the problem locally.
2018-01-24 13:23:35 -05:00
Jarrod Johnson e7a70f390e Treat blank setting for gateway same as unset
It was previously trying to use empty string as gateway in some
contexts.
2018-01-24 10:55:03 -05:00
Jarrod Johnson 4ae32c1219 Merge branch 'master' of github.com:jjohnson42/confluent 2018-01-24 09:58:41 -05:00
Jarrod Johnson d04f6cc858 Accept '-f' as alias for '-c'
In xCAT, '-f' meant 'fanout' and it's harmless for us to support
both.
2018-01-24 09:26:18 -05:00
Jarrod Johnson 03703250e6 Further improve formatting of the discoverystate option 2018-01-23 16:28:07 -05:00
Jarrod Johnson 01dd48ccc2 Fix some man page formatting issues 2018-01-23 16:16:47 -05:00
Jarrod Johnson 91d0c8ed7a Add flag to request discovered devices by discovery state
Also refresh the manpage to reflect the current state of nodediscover
command, and flesh out the discovery states there.
2018-01-23 16:09:17 -05:00
Jarrod Johnson a2163244db Correct indentation 2018-01-23 15:19:31 -05:00
Jarrod Johnson f615232a95 Merge branch 'master' of github.com:jjohnson42/confluent 2018-01-23 15:05:33 -05:00
Jarrod Johnson e7aeece7f4 Add nodediscover clear
Provide means of deleting discovery data, particularly to help rescan.
2018-01-23 15:05:24 -05:00
Jarrod Johnson 19e733f325 Ensure fingerprint variable is initialized
During error handling, there were paths where fingerprint was
needed, but not set.  Fix this by getting fingerprint before raising
the exceptions.
2018-01-23 14:34:06 -05:00
Jarrod Johnson 924f679f79 Add ability to delete discovery entry
This allows a full purge without restarting confluent.
2018-01-23 13:27:20 -05:00
Jarrod Johnson 9baa1f5652 Accommodate XCC firmware behavior
It has been observed that a Lenovo XCC can fail to produce the
appropriate attributes in the SLP data.  In such a case, and only if we
are in the preconfig path (which means it is a candidate for discovery),
reset the XCC to try to correct the behavior.
2018-01-23 13:07:59 -05:00
Jarrod Johnson eccc7803a9 Move the (insecure) output to where promised 2018-01-23 10:36:25 -05:00
Jarrod Johnson cd260a769e Fix variable name
rsp rather than res was used in one place erroneously
2018-01-23 10:32:48 -05:00
Jarrod Johnson 56b6babed6 Provide insecure warning
When certificate validation is not occuring in a secure fashion, make
it obvious to user through '(insecure)' flag.  Lay the groundwork for
future update to actually provide a secure attach strategy, once the
firmware is willing.
2018-01-23 10:23:56 -05:00
Jarrod Johnson 37f0345553 Format the complex epilog of nodemedia
nodemedia has non-option arguments suggesting need for line wraps.
Provide a custom formatter to enable readable usage.
2018-01-22 16:12:29 -05:00
Jarrod Johnson 98a763eb35 Change detach to detachall
It is more clear what detach will do this way.  Also block more
attempts.
2018-01-22 15:56:59 -05:00
Jarrod Johnson 9b7db2a924 Explain the sort algorithm on collate
Provide a more thorough explanation of the sort algorithm
2018-01-22 15:09:19 -05:00
Jarrod Johnson 87696a7b0d Add man page for nodemedia
Documentation to flesh out the nodemedia command
2018-01-22 14:18:03 -05:00
Jarrod Johnson c2a15fc74d Fix typo in nodeconfig man page
The man page mispelled the command in the SYNOPSIS
2018-01-22 14:16:04 -05:00
Jarrod Johnson 97401e306b Tolerate disabled IPv4 multicast route
Sometimes the IPv4 multicast route isn't playing ball.  Tolerate such
a scenario and move on.
2018-01-22 11:53:19 -05:00
Jarrod Johnson 48b9d735f2 Avoid empty hwaddr
It has been observed, at least once, for an empty hwaddr to appear.
Take measures to prevent this from occurring and making into the
discovery info.
2018-01-19 10:22:22 -05:00
Jarrod Johnson 6444756b3c Add list to upload command
Have the upload command do list at the end to give affirmation of
what happened.
2018-01-17 10:06:54 -05:00
Jarrod Johnson af82e868d2 Alter nodemedia command
Add the glob protection and have attach do a list after.
2018-01-17 10:06:05 -05:00
Jarrod Johnson 76818135a6 Fix getting nodenames based on enclosure uuid
Must listify the output and make sure the previous output is fed into
the second filter.
2018-01-16 13:29:36 -05:00
Jarrod Johnson 0f4940cd7c Tolerate empty string to be blank snmp user
For SNMP password without user, accept '' as a synonym for None
2018-01-16 13:14:13 -05:00
Jarrod Johnson 8226c2bd77 Fix mistakes in the draft discovery code
A couple of blatantly obvious mistakes were made that require fixing.
2018-01-16 12:13:34 -05:00
Jarrod Johnson a80ae622f6 Skip SMMs without a stored certificate
If something happens to have the right ip, but no stored certificate
because it's not discovered, it was used as a data source if the
addpolicy was lax.  Harden the flow by skipping unverifiable parts
of the chain.
2018-01-16 10:54:42 -05:00
Jarrod Johnson ea5165d2c5 Fix the TLS verifier to support different fingerprint algos
It was hard baked to sha512.  Now use sha256 if auto-adding,
and use the stored algo as indicator instead.
2018-01-16 10:30:15 -05:00
Jarrod Johnson 30e24cc768 Fix type compatibility with enclosure.bay
enclosure.bay manifests as an int.  Use format() to tolerate either
way.
2018-01-16 10:20:28 -05:00
Jarrod Johnson b1951b3d86 Add error on older XCC firmware
When we do hit a problem with the chained configuration and older
XCC, log a message to show why discovery will forever fail.
2018-01-16 09:18:30 -05:00
Jarrod Johnson c649aa2a40 Avoid traceback on older XCC firmware
Older XCC firmware does not provide chassis-uuid.
2018-01-16 09:16:23 -05:00
Jarrod Johnson e7b97afc3c Address a number of oversights
The null handler case, IPv6 SMM, and SMM cert conversion needed fixes.
2018-01-11 16:48:34 -05:00
Jarrod Johnson 8acb59d967 Fix noderun/nodeshell filehandle exhaustion
noderun/nodeshell failed to close filehandles on child
exit.
2018-01-11 12:40:53 -05:00
Jarrod Johnson a70e3a2e19 Add ability for physical discovery to not root in switch
This permits a design where the switch can be omitted, but only the
head enclosure manager needs 'manual' intervention.
2018-01-11 10:46:23 -05:00
Jarrod Johnson a11a0cd543 Accelerate dependent discovery in D2 enclosures
Rather than wait until the next recheck interval, track and trigger
dependent discovery immediately.
2018-01-11 08:41:44 -05:00
Jarrod Johnson 3398acaf52 Match XCC to enclosure in a chained setup
Also apply fixes and lay groundwork for eventual 'secure' discovery
policy.  As such a policy is too limited to be practical at this point
(SMM only) the full deal is postponed until it would be feasible.
2018-01-10 15:53:49 -05:00
Jarrod Johnson 0c0a450fc2 Implement requisite functions to do chain SMM discovery
Hook into the neighbor data and lldp to identify SMMs.

Still need to provide context to the XCCs based on the chassis-uuid
property.
2018-01-09 17:25:31 -05:00
Jarrod Johnson 0337962cd9 Add lenovo-switch to discovery api
This does not actually allow config deployment, but it can help in
ascertaining access for manual access to a switch.

A proper 'handler' will come later to add configuration, probably with
an emphasis on CNOS rather than ENOS.
2018-01-09 09:17:39 -05:00
Jarrod Johnson 73f40ecbf8 Add a dir2img command to help with generation of RDOC
RDOC requires a valid 'floppy-like' image, this script aids in the
creation of such a thing.
2018-01-04 14:17:27 -05:00
Jarrod Johnson 9f5b88eb9f Fix nodemedia upload
Implement the tracking properly
2018-01-04 13:39:51 -05:00
Jarrod Johnson 3265d812ba Tweak the media implementation
Change key to 'detachall' for now and remove the 'all' argument from
detach.
2018-01-04 13:21:22 -05:00
Jarrod Johnson 673fb02896 Draft nodemedia command
Server side still has some limitations, but it functions when
going downhill with a tailwind while spewing error messages.
2018-01-03 15:35:08 -05:00
Jarrod Johnson 0d40a0cac6 Add ability to clear upload progress tracking
Remote media uploads could not be cleared, add the ability to do so.
2017-12-08 18:58:04 -05:00
Jarrod Johnson 6845f64d46 Correct spelling of tenant attribute
The attribute had been mispelled and needed a fix.
2017-12-08 18:42:08 -05:00
Jarrod Johnson ade1d93071 Fix strip_node on createdresource 2017-12-08 18:35:39 -05:00
Jarrod Johnson b18ad89672 Correct some errors in the previous commit 2017-12-08 18:27:35 -05:00
Jarrod Johnson 09d20ea1ff Add remote media handling to API 2017-12-08 15:03:27 -05:00
Jarrod Johnson ae39a84a30 Improved unrecognized memory format 2017-12-07 16:54:35 -05:00
Jarrod Johnson 0ae315a12f Fix bad SPD handling
If the memory is not DDR3/DDR4, cease further attempt to format
data.
2017-12-07 16:51:55 -05:00
Jarrod Johnson 60e8338b47 Fix logout
The logout was broken by the forwarder security hardening
2017-11-28 15:48:35 -05:00
Jarrod Johnson 8897eb5dcf Fix user api
The user api had been messed up.  Correct by using keyword to correctly
place the configmanager argument.
2017-11-28 15:23:27 -05:00
Jarrod Johnson 208eb46cc2 Merge branch 'master' of github.com:jjohnson42/confluent 2017-11-28 13:42:32 -05:00
Jarrod Johnson 2c5432454a Implement a nicer nodereseat unsupported command
This carries the error condition cleanly back to the command line.
2017-11-28 13:42:21 -05:00
Jarrod Johnson c5dd024557 Move the switch discoverable check for non-SMM into eval_node
eval_node can establish that this is a direct discovery attempt.
In that specific context, the check can be performed.  Otherwise,
we can't check in this way, but the enclosure manager should raise the
error on behalf of the rest of the situation.
2017-11-27 16:59:13 -05:00
Jarrod Johnson 72af8f1631 Fix custom and net.* attributes for groups and alias clearing
The _group function was not using fixup_attribute, add that.

Additionally, on the clear_ functions, use the aliases to make clearing
work with the shorthand as well.
2017-11-27 10:36:29 -05:00
Jarrod Johnson d69cca46d0 Rework check_globbing to reduce false positives
First, globbing can only be the cause of a mess up if the given
noderange is a file that matches.

With this we still have:
for node in $(nodelist compute); do nodepower $node; done
As a potential false positive if any node is a range.

For this, offer suggestion of changing directories.

Also, if it had been:
for NODE in $(nodelist compute); do export NODE; nodepower $NODE; done

Another clause can detect that, which has been added.
2017-11-27 10:04:23 -05:00
Jarrod Johnson 4bef5f7917 Merge branch 'master' of github.com:jjohnson42/confluent 2017-11-15 15:39:06 -05:00
Jarrod Johnson 03293d88b0 Have nodeeventlog print help on incorrect arguments 2017-11-15 15:38:59 -05:00
Jarrod Johnson b78266eff7 Merge branch 'master' of github.com:jjohnson42/confluent 2017-11-15 09:41:28 -05:00
Jarrod Johnson e63d7f9fe3 Correct typo in the nodeshell command
The November 6th change contained a typo.
2017-11-15 09:41:01 -05:00
Jarrod Johnson 16297b048f Add nodegroupdefine and nodegroupremove
With these, a user can noow largely ignore confetty for most
abosuletly universal functions.
2017-11-14 16:05:03 -05:00
Jarrod Johnson 6d88dbb374 Add missing man pages
Several commands did not yet have man pages, address this documentation
issue.
2017-11-14 15:55:03 -05:00
Jarrod Johnson 43c5ecd6ae Fix missing aliases 2017-11-14 15:16:58 -05:00
Jarrod Johnson 9d2ec60b50 Set executable bits on various commands 2017-11-14 14:59:37 -05:00
Jarrod Johnson a16d4b12ea Merge branch 'master' of github.com:jjohnson42/confluent 2017-11-14 14:29:57 -05:00
Jarrod Johnson 76bfb29d60 Try to put the brakes on too many mac discovery attempts
In various scenarios, too many macs on a port can be a sign of trouble.
For example, a chained SMM configuration with head on switch port, or
incorrectly pointing a nodes net attributes at a switch uplink port, or
defining SMMs without any nodes, causing XCCs to think they are
rackmount.  This sets some sanity value for avoiding problems.  This is
of course a mitigation, invalid scenarios could still run afoul of the
limits, but it should catch a large chunk of offending scenarios.
2017-11-14 14:28:31 -05:00
Jarrod Johnson 3e201a5f4f Back off aborted use of configmanager in nodegroups
Currently, we don't need it and the change was half-attempted.  Remove
the problematic half that remained.
2017-11-14 12:01:57 -05:00
Jarrod Johnson 0b0f325240 Shorten power check interval
The interval can be shorter with little ill-effect.
2017-11-13 14:22:30 -05:00
Jarrod Johnson 29330aee74 Clear [powered off] from screen if state change detected
Previously, only transition to powered on was detected.  React
to situations where data comes up or power status change.
2017-11-13 14:04:09 -05:00
Jarrod Johnson 78dea26d06 Switch glob suppression to detection
The suppression was unable to be accomplished for bash without
somehow otherwise breaking the shell.  zsh and csh could be better at
one-off glob disabling though.
2017-11-13 11:49:40 -05:00
Jarrod Johnson a00747c79c Update nodeconsole man page 2017-11-10 10:37:48 -05:00
Jarrod Johnson 4bbc05699e Fixed log age out with size and time rolling
There was a problem if both size and time based age out were triggering
and failing to honor backupCount.
2017-11-10 08:48:36 -05:00
Jarrod Johnson 06eb91c355 Fix formatting of nodediscover synopsis
Markdown's annoying use of trailing whitespace strikes again.
2017-11-09 16:47:49 -05:00
Jarrod Johnson 5b52582302 Merge branch 'master' of github.com:jjohnson42/confluent 2017-11-09 16:31:29 -05:00
Jarrod Johnson 4de797be05 Add man page for nodediscover 2017-11-09 16:31:11 -05:00
Jarrod Johnson 80864d78b3 Remove unused nestedmode
Without the ability to reliably tell a nodename from an input key,
must not do the nestedmode detection.
2017-11-08 13:36:36 -05:00
Jarrod Johnson cc4950ef75 Opportunistically grab and sort by node
If output comes close enough together, make some effort to group it
so that it will have a higher tendency of looking orderly.  This of
course only does so when it does not interfere with quickly presenting
the data.
2017-11-06 15:47:59 -05:00
Jarrod Johnson fcae11bf96 Do natural sort on expression expansion
This creates more logical behavior from nodeshell and noderun when
dealing with many nodes, particularly when crossing the concurrency
limit.
2017-11-06 14:57:15 -05:00
Jarrod Johnson f5889e7029 Add '-c' to manage noderun/nodeshell concurrency
Allow user to specify custom parallel count.
2017-11-06 14:51:59 -05:00
Jarrod Johnson 148329dd8e Implement send resize commands
Craft correct stty commands based on terminal size
detected by confluent.
2017-11-06 11:09:55 -05:00
Jarrod Johnson d9ffa10422 Restore power query function
Implement it in a fashion that does not block confetty while long
running power query situations happen.
2017-11-06 10:24:32 -05:00
Jarrod Johnson a6bae944a2 Explicit dependency version in rpm
Installs without update can end up with non-viable old versions.

Force yum to try to update downlevel packages if present.
2017-11-06 09:20:12 -05:00
Jarrod Johnson 65bcb8a35b Add glob supressing aliases for new commands
Also, document what they are for the curious.
2017-11-03 16:59:27 -04:00
Jarrod Johnson 31a2fce521 Add noderemove command
Easy deletion of nodes from the confluent database.
2017-11-03 16:54:07 -04:00
Jarrod Johnson ea49108c78 Add nodedefine command
Provide a quick command to define new nodes to confluent direct
from cli without confetty.
2017-11-03 16:39:46 -04:00
Jarrod Johnson 21fce4098b Add ability to list all configuration supported 2017-11-03 10:36:32 -04:00
Jarrod Johnson 8eca6b156b Automatically request static when setting ip
If someone is setting IP address, they are implying static.
Change that implicit reality to explicit statement.
2017-11-03 10:31:00 -04:00
Jarrod Johnson 7f4ea287bc Fix issues in expression enabled InputNetworkConfiguration
This enables setting using expressions for any network configuration
functionality.
2017-11-03 10:25:26 -04:00
Jarrod Johnson 9f3fe01a49 Evolve the concept, it does not currently work 2017-11-02 17:07:44 -04:00
Jarrod Johnson de6ecdab38 Start work on nodeconfig command
This reworks the client to reuse some bits of nodeattrib, and has
nodeconfig able to read values.
2017-11-01 16:48:17 -04:00
Jarrod Johnson 02018da187 Add aliases to the client library
Replicate for the sake of retrieving values by aliased names.
2017-11-01 13:33:57 -04:00
Jarrod Johnson 8bc766e740 Add concept of aliases for setting and expressions
For the moment this allows some shortcuts on setting and
expressions for {bmc}.  Other attribute aliases may come.
2017-11-01 13:28:46 -04:00
Jarrod Johnson 8c21c59459 Dynamic backoff for switch scan
Have switch scan back off propritonate to time it takes to walk the
switches.  Avoid being back to back on switch scans.
2017-10-27 16:50:39 -04:00
Jarrod Johnson d5be1ccf8c Restrict lifetime of port relay to session
If a session is closed, also kill off any associated
relays in progress.  One exception, video port relay
in ESTABLISHED is left alone due to limitation, but
at least no new open.
2017-10-27 14:47:10 -04:00
Jarrod Johnson 45b8a18f14 Merge branch 'master' of github.com:jjohnson42/confluent 2017-10-26 13:31:36 -04:00
Jarrod Johnson d502cd5159 Add nodereseat command to confluent command 2017-10-26 11:48:42 -04:00
Jarrod Johnson 52673a990b Confluent server side enclosure reseat
This provides ability to request reseat of nodes, and redirects it to
the enclosure manager.
2017-10-26 11:45:16 -04:00
Jarrod Johnson 43c60bc180 Add another invalid uuid
This is another uuid that will not be unique that may appear in some
systems without a valid uuid.
2017-10-25 16:16:00 -04:00
Jarrod Johnson c86d9f3e33 More specifically restrict connectivity
When serving multiple browser, limit a forwarder to only the specific
client that authorized that forwarder.  Previously, one client was
allowed to access another client's forwarding port if it happened to
know the location.
2017-10-23 09:34:29 -04:00
Jarrod Johnson 68221b8158 Provide distinct video forwarding per client ip
While we are limited to one 'listen' target at a time, we can
qualiify by the source address to at least provide distinct
behavior depending on the client.
2017-10-23 09:28:24 -04:00
Jarrod Johnson 34fb159801 Restrict forward source ip to requestor
This prevents sockets from opening up to the world that could be used
to connect to management interfaces directly, apart from the specific
requestors.
2017-10-22 12:35:12 -04:00
Jarrod Johnson 3372a8401a Move registration of key handler up to constructor
This should be a more bulletproof place to be.  Note that it used
to be here and was moved because pyghmi used to call oem_init, but
pyghmi has been changed for a long time to no longer have that
requirement.
2017-10-20 15:18:03 -04:00
Jarrod Johnson f07a0e333f Use natural sort in parts of macmap api
It was hard to traverse things through default sort, provide
natural sort to node names and port names.
2017-10-20 15:15:59 -04:00
Jarrod Johnson 301014f3b5 Fix nodediscover rescan
Actually request that the server do rescan, rather than queueing it up
and failing to transmit.
2017-10-20 13:53:13 -04:00
Jarrod Johnson aa0255bf62 Limit firmware updates to 256 at a time
To keep the concurrent update level to a fairly widely attainable limit,
impose 256 at a time limit.  This is a gigantic improvement over most
previous circumstances still, and can comfortably accommodate three of
the most dense racks at a time.
2017-10-20 08:43:43 -04:00
Jarrod Johnson 444ba917eb Have node*attrib commands error if user does -c and =
Can't clear and assign in one go, error for user awareness.
2017-10-19 10:02:30 -04:00
Jarrod Johnson 6659b31106 Provide graceful error message on cert mismatch
Firmware update can provide a more clean looking message on the
event of a cert mismatch.  Improve polish by doing it this way.
2017-10-18 12:58:21 -04:00
Jarrod Johnson 3e93cb2434 Stub out strip_node for DeletedResource
This is only ever a URL at the moment.  Do not trigger stack
traces in firmware update by stubbing out the strip_node.
2017-10-18 10:58:17 -04:00
Jarrod Johnson ffaabd5ae3 Do not process incomplete DIMM info
A malformed SPD given by BMC is now presented as Unrecognized,
rather than trying to format data that does not exist.
2017-10-18 09:51:31 -04:00
Jarrod Johnson d26f6259a7 Add explicit error about certificate mismatch
Before we were aborting without info.  Now explain the missing
information by including the error explaining why it is missing.
2017-10-17 13:46:40 -04:00
Jarrod Johnson 757df0f571 Reduce noise of one off reading
Per request, limit the display precision of floats in the nodesensors
command readout.
2017-10-17 11:32:17 -04:00
Jarrod Johnson bfc2b65e99 Support 'energy' sensor category
Energy is distinct from power, categorize it as such.
2017-10-17 11:07:54 -04:00
Jarrod Johnson 3a1e9fe0bc Fix typo in function name 2017-10-16 11:21:18 -04:00
Jarrod Johnson bb03e4a961 Fix HTML view for simple string values
Simple string values were incorrectly treated in the HTML view.
Fix it so the data is actually readable.
2017-10-16 10:38:13 -04:00
Jarrod Johnson f7b5280d6a Fix HTML view with bool value
HTML view was causing internal server errors when the api provided
boolean data
2017-10-16 10:35:47 -04:00
Jarrod Johnson f1f6c3b066 Correct syntax error
The prior commit had a grave syntax error.
2017-10-16 10:30:02 -04:00
Jarrod Johnson b7b26a1069 Merge branch 'master' of github.com:jjohnson42/confluent 2017-10-16 10:21:15 -04:00
Jarrod Johnson e3b869fa34 Fix minor formatting issue 2017-10-16 10:21:01 -04:00
Jarrod Johnson a962a5afc3 Exempt Web Forwarding from CSRF
Web forward from popup context requires it be exempt from CSRF
protection.  This is harmless (as all GET should be, but erring on side
of caution), so add it to the whitelist of CSRF skipping on a CSRF
enabled client session.
2017-10-16 10:20:49 -04:00
Jarrod Johnson b2fdaeffe7 Add 'Power' to relevant categories for power
While IPMI has no 'Power', some OEM sensors use that
type anyway.
2017-10-13 16:08:59 -04:00
Jarrod Johnson 8694eca40b Error out a whole noderange if global error occurs
For example, nodeboot with invalid argument failed and then proceeded
to 'boot' phase.
2017-10-13 09:08:12 -04:00
Jarrod Johnson d5efa87288 Add history to the dbg cli helper
As long as we are adding readline, history persistence is also really
handy.
2017-10-12 14:15:36 -04:00
Jarrod Johnson 6a39a3f9a2 Fix html api browser add/assign resource
The html extraction was unable to process the message.
2017-10-10 09:16:06 -04:00
Jarrod Johnson 9b1e70893b Merge branch 'master' of github.com:jjohnson42/confluent 2017-10-10 09:13:21 -04:00
Jarrod Johnson 672c127621 Fix error propogation on manual discovery
Manual discovery needs to give the user, not the log info about the
failure.
2017-10-06 16:10:47 -04:00
Jarrod Johnson 158a9705db Support full assign from csv input 2017-10-06 14:34:34 -04:00
Jarrod Johnson e7c6dfab2b Fix list by invalid type
Rather than list everything, list nothing
when asked to list an invalid type.
2017-10-06 14:02:49 -04:00
Jarrod Johnson c891cff926 Phase 1 of bulk assignment
Parse CSV and do some validation, next phase will actually
create nodes and assign the discovery.
2017-10-05 16:55:11 -04:00
Jarrod Johnson a8a32118db Add the ability to assign
Start with the ability to assign one at a time.
2017-10-05 13:52:20 -04:00
Jarrod Johnson fdc4e959f7 Populate nodename on pxe entries when possible
If a PXE is matched by UUID, populate the nodename just like the
serial number and such.
2017-10-05 10:16:49 -04:00
Jarrod Johnson 91ff08158f Add rescan subcommand to nodediscover
Allow nodediscover to initiate an active scan.
2017-10-05 10:05:56 -04:00
Jarrod Johnson 79b47bd0b7 Create nodediscover command
Provide a command to interact with discovery data in a more convenient
way.  This commit comprises at least most of the ability to list and
filter.
2017-10-04 16:27:40 -04:00
Jarrod Johnson c95d3cf906 Note for future chassis id relationship 2017-10-04 09:43:35 -04:00
Jarrod Johnson 9c6b10a166 Sort output from confetty
Sorting can greatly improve the readability of some
confluent functionality.
2017-10-04 09:09:03 -04:00
Jarrod Johnson db89cdc62c Add portid and chassisid for cross-reference
In the hypothetical of generating a graph, having a hard assurance
of the required LLDP TLVs can aid in viable code.
2017-10-03 14:57:32 -04:00
Jarrod Johnson 697716b296 Provide a cleaner error on peer not found
Auto-refresh and provide cleaner error on ultimately missing data.
2017-10-03 11:52:28 -04:00
Jarrod Johnson 60b7083dce Handle URL safety of LLDP data
LLDP data does not care about URL safety.  Have a mechanism that
does a pretty good job of translating too and from, at some cost.
2017-10-03 11:23:57 -04:00
Jarrod Johnson f4cfe8758e Fixes to the recent attempt
Names had '/' in it and some assumptions about key were broken.  Correct
the various mistakes.
2017-10-03 10:30:18 -04:00
Jarrod Johnson cc0fc52064 Reuse neighbor data
Improve responsiveness by only refetching data if the data is over
a minute old.
2017-10-03 10:01:57 -04:00
Jarrod Johnson 526eb45969 Fix to match all critera than any.
It was matching *any* of the criteria, it needed to match all.
2017-10-02 17:04:38 -04:00
Jarrod Johnson 3606aab3f0 Only director-ify multiple entry type selectors 2017-10-02 17:01:43 -04:00
Jarrod Johnson eb70b21573 Properly reflect results as collections/directories 2017-10-02 16:56:52 -04:00
Jarrod Johnson e95b5da8b7 Wire up peerid
The entries did not have peerid in them, rectify the oversight.
2017-10-02 16:56:17 -04:00
Jarrod Johnson b5e4d302f9 Actually wire up the parameterized version 2017-10-02 16:53:27 -04:00
Jarrod Johnson 1fb6eb230b Change mind about peerid
Stick to the identifiers mandated by LLDP, and only those to
base the peerid (by-name can help otherwise).
2017-10-02 13:32:56 -04:00
Jarrod Johnson e298ead944 Merge branch 'master' into betterneighbor 2017-10-02 13:28:33 -04:00
Jarrod Johnson c0ab8532df Fix noderun when invalid noderange passed
exitcode was not yet set when starting to be referenced.  Initialize to
zero prior to any possible reference to fix.
2017-10-02 09:07:06 -04:00
Jarrod Johnson 4e1ebc290b Fix noderun when invalid noderange passed
exitcode was not yet set when starting to be referenced.  Initialize to
zero prior to any possible reference to fix.
2017-10-02 09:06:29 -04:00
Jarrod Johnson 4236f3fd78 Work on a better neighbor interface
The API was lacking in imagination, flesh it out more.
2017-09-28 17:09:05 -04:00
Jarrod Johnson d4265fa406 Normalize likely HEX strings
Chassis Id and Port Id are highly likely to present as hex strings,
detect and present this case more sanely.
2017-09-28 16:28:33 -04:00
Jarrod Johnson 563ee96879 Harden confetty to weird text strings
If it can't properly encode to printable, use repr instead.
2017-09-28 16:20:19 -04:00
Jarrod Johnson 529e3b2239 Fix a few issues found in testing
Switches need some data sanitized.  Also used IndexError instead of
KeyError, and add handling for interfaces with slashes in the name.
The name 'verified' also fits better than 'authenticated'
2017-09-28 14:27:34 -04:00
Jarrod Johnson 2ed80d86b1 Implement proper neighbor structure
Also fix a bug in macmap and assume SHA auth protocol for SNMPv3
2017-09-27 15:03:09 -04:00
Jarrod Johnson 317809f449 Further refactor and mature neighbor data
At this point, the /networknig/neighbors mostly works...
2017-09-27 14:24:00 -04:00
Jarrod Johnson ea27125587 Begin implementing the 'neighbors' collection
Refactor some common function and list interfaces on a switch
2017-09-26 16:56:45 -04:00
Jarrod Johnson 0741e3953e List switches if known even without macs
The macs was only showing switches with currently known mac address.
Provide all the data (and perhaps a hook to geet errors later).
2017-09-26 12:13:01 -04:00
Jarrod Johnson f44eb412c5 Add /nodes/<node>/forward/web
This facilitates GUI to launch web interface,
even if the target is unreachable.
2017-09-22 13:53:37 -04:00
Jarrod Johnson c571601a29 Add utility library to do port forwarding
This will be useful for creating forwarding for
users.
2017-09-22 10:59:37 -04:00
Jarrod Johnson dad86242a9 Performance optimize abbreviation
Don't abbreviate the same nodes twice when we don't have to.
2017-09-22 10:06:02 -04:00
Jarrod Johnson a0802dd66e Add natural sort
For output groups with equivalent node counts, sort naturally
by the group text header.
2017-09-22 09:54:28 -04:00
Jarrod Johnson c966b09581 Fix behavior on firmware update timeout
nodefirmware was not handling results where the update couldn't even
start.  Handle those errors properly.
2017-09-22 09:32:43 -04:00
Jarrod Johnson b631699656 Correct incorrect bank parameter on nodefirmware -b
The bank should have been 'backup', not 'bank' which makes no sense.
2017-09-22 09:06:42 -04:00
Jarrod Johnson aa6857b1c7 Do not consider refusal of default creds fatal
During discovery, there's a chance that user/pass already set.
For now, do not fret over this and just silently fail to enable
enclosure manager.
2017-09-19 15:22:40 -04:00
Jarrod Johnson 9c5f50341f Various fixes to the previous attempt 2017-09-18 14:51:43 -04:00
Jarrod Johnson eb1b80f2c9 Get UUID from enclosure and set
During config, take the opportunity to update uuid for enclosure.manager
if possible.
2017-09-15 17:00:36 -04:00
Jarrod Johnson 368666823f Modify get_nodename to search by uuid
Instead of *just* being able to search by uuid for pxe,
add the ability to search by uuid for https things, if no fingerprint
match.

This allows remote xcc setup to follow in the wake of a pxe attempt.
2017-09-15 12:00:10 -04:00
Jarrod Johnson d8216f2472 Add another invalid UUID
All spaces is yet another bad UUID that has cropped up...
2017-09-15 11:14:00 -04:00
Jarrod Johnson 0db2e1cea6 Add clarification on what noderange is for 2017-09-12 15:29:59 -04:00
Jarrod Johnson 6a1f7e7fa4 Fix for incorrect uuid format
In the slp data, the data is an array, make it a string first.
2017-09-11 16:42:56 -04:00
Jarrod Johnson e2e4c8c7e1 Add SMM UUID to info when available 2017-09-11 15:21:55 -04:00
Jarrod Johnson ee7e044ad8 Add missing socket import
The Non-sysfs fallback needs to have socket imported to function
2017-09-05 11:20:42 -04:00
Jarrod Johnson 12cbfff24f Fix for unexpected entries in /sys/class/net
For example, in a bonding configuration there is a file there, not just
directories with ifindex entries.
2017-09-05 09:19:15 -04:00
Jarrod Johnson 8774ae0305 Avoid linking obviously bad UUIDs
If UUID is obviously bad, do more to ignore it when linking data
2017-08-26 23:20:45 -04:00
Jarrod Johnson cb00c5d35d Place limits on discovery resource consumption
Situation of filehandle exhaustion was seen.  In the scenario observed,
multiple connections to the same target were seen.  So:

1) Backout the recheck block and replace with more comprehensively
placed semaphore.

2) Place a discovery pool limit of 500 to generally constrain things.

3) Further limit things to one detected thread per mac address
2017-08-26 22:45:36 -04:00
Jarrod Johnson 2a0b038c30 Merge pull request #83 from aduffy19/noderange
Add noderange man page
2017-08-25 16:42:12 -04:00
Jarrod Johnson deb2c3a94a Correct some minor potential issues and add restart on failure 2017-08-25 16:41:22 -04:00
Amanda Duffy 3f1bb1c151 Add noderange man page 2017-08-25 16:39:07 -04:00
Jarrod Johnson 0e708f4b26 Back off https recheck
With various fixes, it does not make as much sense to be so aggressive
with rechecking https assuming we sense something prior to https
being up.
2017-08-25 09:24:21 -04:00
Jarrod Johnson 5c93976bdb Fix errant certificate handling
Non-replacement detection was being treated as replacement.  Correct
that mistake.
2017-08-24 16:57:06 -04:00
Jarrod Johnson 312af72d9f Fix policy handling if not set 2017-08-24 16:34:57 -04:00
Jarrod Johnson 19f75474fc Fix db restore reliability if replacing existing
If there is an existing DB, there were race conditions in loading the
restore.  Fix by clearing the existing DB once the viability of the
restore dataset has been established
2017-08-24 16:04:04 -04:00
Jarrod Johnson 695e038ced Skip attempt to fixup custom attributes 2017-08-24 16:01:00 -04:00
Jarrod Johnson a98d8dc43c Provide capture of PXE mac addresses to net*.hwaddr
This provides a persistent store of PXE mac addresses for consumption
by other provisioning systems that are generally MAC centric.
2017-08-24 15:42:40 -04:00
Jarrod Johnson 69cbeecf1b Correct fixup for untyped data 2017-08-24 13:44:21 -04:00
Jarrod Johnson 5cd461c6ad Add understanding of net.* attributes to fixup
net.* are special, add them to the fxup so that the bootable attribute
is correctly amended.
2017-08-24 13:26:38 -04:00
Jarrod Johnson de30c53d21 Add net.*.bootable
Provide a flag to indicate that a nic entry is meant to hold
a bootable network interface, for purpose of holding a PXE detected
mac.
2017-08-24 13:15:32 -04:00
Jarrod Johnson a599c346f8 Add support for boolean attributes
Normalize likely textual input to a boolean, but otherwise treat such
data in the same manner as a string.
2017-08-24 13:14:23 -04:00
Jarrod Johnson 1e88ce2d41 Flesh out pxe client info if matching discovery data found
While we cannot get serial number and model number in a PXE request,
if there is a matching uuid, fill in the data from the match.  Also
provide info on the related macs so that info can be correlated in more
complex ways (e.g. which mac is pxe, which mac is BMC, which IP goes
where, etc.)
2017-08-24 10:23:43 -04:00
Jarrod Johnson d337de671e Add missing uuid list generator link
The UUID mapping was not connected fully to the API.
2017-08-23 15:52:20 -04:00
Jarrod Johnson 68c7ab4c90 Add ability to group discovery by uuid
API adds 'by-uuid', which helps group BMC and PXE together to make
relationship clear.
2017-08-23 15:41:16 -04:00
Jarrod Johnson 4af5e2d615 Correct uuid retrieval mistake 2017-08-23 13:17:18 -04:00
Jarrod Johnson 299c5c4e71 Add UUID to returned discovery data when available 2017-08-23 12:57:34 -04:00
Jarrod Johnson e5c330bb1c Be less verbose about non-viable data in periodic recheck
If we have a device that has aged out of usable addresses, do not pester
log about this incessantly.
2017-08-23 12:47:44 -04:00
Jarrod Johnson e1eafa46f9 Be robust in handler init withotu addresses set 2017-08-23 11:16:07 -04:00
Jarrod Johnson 9798f30ef8 Remove codecs getwriter unicode from nodesensors
In nodesensors, things are made into binary strings explicitly,
which causes the codecs.getwriter to choke
2017-08-22 16:54:25 -04:00
Jarrod Johnson d1bafa98bb Merge branch 'master' of github.com:jjohnson42/confluent 2017-08-22 16:50:20 -04:00
Jarrod Johnson 2cdfa8fcae Do not let a single node error stop all pending node
Isolate faults to single node for better robust behavior.
2017-08-22 16:50:04 -04:00
Jarrod Johnson b93a2e60d2 Be more selective in IP pruning
Only prune known bad IPs, not the whole list
2017-08-22 16:47:56 -04:00
Jarrod Johnson e478f8aef3 Add --backup to nodefirmware man page 2017-08-16 17:38:52 -04:00
Jarrod Johnson b35463e594 Implement argument to nodefirmware to backup 2017-08-16 17:33:23 -04:00
Jarrod Johnson 54f25cfa9e Implement access to other banks
Provide backend support for other banks than primary.
2017-08-16 17:30:18 -04:00
Jarrod Johnson 9ff7829373 Fix typo in confetty
Strings do not have a startswitch method...
2017-08-16 15:06:48 -04:00
Jarrod Johnson f3b56cf187 Add abbreviate to man page
abbreviate was not in the man page for collate.
2017-08-16 11:34:36 -04:00
Jarrod Johnson 783837b9be Further collate page formatting fixes 2017-08-16 10:42:44 -04:00
Jarrod Johnson 1b18edcccb Fix formatting issues in collate man page 2017-08-16 10:31:58 -04:00
Jarrod Johnson d972e2c12c Fix man page for section 1 2017-08-16 10:12:03 -04:00
Jarrod Johnson c5cf829117 Avoid stacktrace on pipe on non-ascii
Various commands may (accidentally or intentionally) encounter
non-ascii data.  While python stdout without pipe is fine, when piping
it assumes ascii.  Fix this to always assume utf-8 explicitly.
2017-08-16 09:57:57 -04:00
Jarrod Johnson 2f55a251cd Provide more graceful behavior when input has no ': '
Rather than do a stacktrace, place such data into an 'UNKNOWN' output
group, like xcoll did.
2017-08-16 09:44:22 -04:00
Jarrod Johnson d63f6ec9f0 Fix install of collate man page 2017-08-16 09:42:30 -04:00
Jarrod Johnson 9d582262e6 Update man page information
Document use of collate and firmware update
2017-08-15 16:17:28 -04:00
Jarrod Johnson 2063436ccc Reduce context of collate diff
diff mode is less useful with superfluous context.
Suppress context to focus more on differences.
2017-08-15 16:11:38 -04:00
Jarrod Johnson 6addd05de2 Stop firmware when 'pending', which is another 'completion' 2017-08-14 17:10:42 -04:00
Jarrod Johnson 28259511ed Add abbreviate to collate
Also, perform natural sort on 'nodenames' when not abbreviating
2017-08-14 15:06:57 -04:00
Jarrod Johnson 0393e55eb1 Implement API for reverse noderange
Provide reverse noderange through special
/noderange//abbreviate location.  This should enable
collate to abbreviate noderanges.
2017-08-14 14:41:43 -04:00
Jarrod Johnson f7b964b2ce Implement feedback on final state of firmware
Some things on update are active immediately, others are pending reboot.
Documentation needs to use this to let users know what they need to do
or not need to do after the firmware update.
2017-08-14 10:04:19 -04:00
Jarrod Johnson da0941c229 Suppress noise of sysctl command 2017-08-11 16:37:09 -04:00
Jarrod Johnson 7cec311b22 Fix sysctl packaging info 2017-08-11 16:11:05 -04:00
Jarrod Johnson 8ada0e7cf4 Read in sysctl tune on install
This ensures that the value is ready for slp plugin
2017-08-11 15:45:56 -04:00
Jarrod Johnson 87e2e481c2 Tune the UDP receive buffer
We have a sysctl tune file and option to
go higher so that we get more SLP responses.
2017-08-11 15:43:44 -04:00
Jarrod Johnson 38546514e2 Pull UUID into info for rackmount
UUID was erroneously not pulled.  The block on dense/blade should
only apply to enclosure bay.
2017-08-11 10:07:42 -04:00
Jarrod Johnson 1e54abd29b Implement reverse noderange
This is the library feature.  A remote API call will be addded
so that the likes of collate can use it.
2017-08-10 16:45:38 -04:00
Jarrod Johnson a571faa215 Implement collate command
Also adjust output to be less buffered for sake of '-w' and other
pipe commands.
2017-08-09 17:08:52 -04:00
Jarrod Johnson 87da7b62ae Correct mistake in the model attribute name
The info calls it 'modelnumber' rather than 'model'
2017-08-09 14:55:27 -04:00
Jarrod Johnson 8d02d7cc7f Store serial and model number in id attributes on discovery 2017-08-09 14:31:29 -04:00
Jarrod Johnson 5f1f41b975 Add serial and model number to the id attributes
Lay groundwork for pulling this sort of data in on discovery.  The plan is that *if* serial numbers will
be used as a cue for discovery, it would be in the context of a nodediscover command.
2017-08-08 15:51:27 -04:00
Jarrod Johnson 7c1bbe1618 Fix error in the usage text of nodeinventory 2017-08-08 15:45:53 -04:00
Jarrod Johnson 76e6f2dd17 Update online usage message for nodeinventory 2017-08-08 11:56:06 -04:00
Jarrod Johnson 67f2282cc4 Update nodeinventory page with new usage info 2017-08-08 11:55:31 -04:00
Jarrod Johnson b98eea81ad Add filtering to nodeinventory
Allow filtering output by various parameters.
2017-08-08 11:49:50 -04:00
Jarrod Johnson 3bf84ee1e7 Merge branch 'master' of github.com:jjohnson42/confluent 2017-08-08 10:39:49 -04:00
Jarrod Johnson 498803f70a Fix line wrap in nodebmcreset example
Again, the curious behavior of markdown for line break
to be two spaces at the end bites some documentation.
2017-08-08 08:36:21 -04:00
Jarrod Johnson 9ac7158bdd Fix SMM enablement on XCC in SD530
The form factor is, at least in one test, an array, not a string.  Just in case, tolerate
either way.
2017-08-07 13:46:14 -04:00
Jarrod Johnson 4110d18f43 Fix exitcode init in noderun
The order was incorrect in the code.
2017-08-07 11:42:58 -04:00
Jarrod Johnson efe3db11ce Add trigger for restarting on pyghmi updates
Often times, a pyghmi update carries the substance of
a patch.  Instead of a person having to remember to
manually restart, try to trigger an update to restart
confluent automatically.
2017-08-07 09:51:04 -04:00
Jarrod Johnson c8fbb58ae9 Correct overzealous deletion
Selection error caused if statement to have syntax error.
2017-08-03 09:41:21 -04:00
Jarrod Johnson 05715c6826 Fix noderun in same way as nodeshell
noderun had a similar issue about error handling.
2017-08-03 08:40:58 -04:00
Jarrod Johnson 5d7df6dceb Fix error handling in nodeshell
nodeshell needed to process errors correctly, and not assume databynode would be there.
2017-08-03 08:39:04 -04:00
Jarrod Johnson dbcc33629a Fix nodeshell overeager parsing
In the case of execing to ssh, preserve literal argumants since
ssh is going to parse them.  Pre-parsing will cause ssh to strip out spaces, for example.
2017-08-03 08:33:29 -04:00
Jarrod Johnson 085a9fed37 Do not error if debug or usr1 handling is impossible
Windows is a limited platform, let it run regardless.
2017-08-02 16:43:08 -04:00
Jarrod Johnson 659c4e4217 Avoid trace on deleted node
If a node is deleted, act similar to if it were defined with no console.method, to avoid
superfluous trace output.  In the future, it may make sense to filter out nodes with no
console.method earlier, since a fair amount of startup work is done that is ultimately ignored
for situations where console is not enabled.
2017-08-02 10:00:16 -04:00
Jarrod Johnson 28ba40ccfa Replace SOL locknig with NullLock
Again, eventlet monkeypatching of pyghmi seems to be messing up
use of RLocks...
2017-07-31 10:05:50 -04:00
Jarrod Johnson e2b7440a5c Put id of thread in the trace
When looking at the traces, handy to have ids for each thread for various
purposes.
2017-07-31 10:02:05 -04:00
Jarrod Johnson a55ba02d11 Merge remote-tracking branch 'upstream/master' 2017-07-28 16:05:45 -04:00
Jarrod Johnson 2ef549c2ae Correct the NullLock
NullLock was not what util.protect was expecting, provide more function calls, all of which do nothing.
2017-07-28 16:05:10 -04:00
Jarrod Johnson 8123a77451 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-28 15:53:25 -04:00
Jarrod Johnson 9e44087047 Breakup logentries exceeding 65k
The data length of a log entry must not exceed 65k.  If an attempt is
made to log that much, break it up and duplicate the records.  It may make
sense to indicate a continuation explicitly, but for now just extend.
2017-07-28 15:53:07 -04:00
Jarrod Johnson 6e51e02e1b Merge pull request #81 from aduffy19/nodebmcreset
Nodebmcreset
2017-07-27 15:03:08 -04:00
Amanda Duffy 3e90dd6462 Pass sys.exit() an argument
Change sys.exit() to sys.exit(success)
2017-07-27 14:31:37 -04:00
Jarrod Johnson 4a929eb9b5 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-27 09:03:53 -04:00
Jarrod Johnson 858647bb6d Change to str for exception message
str will tend to present a more normal looking error string.  Use
that so that a user does not have the impression there is a code
issue on expected errors.
2017-07-27 09:03:36 -04:00
Jarrod Johnson 8a416ccc5e Correct mistake in core formatting 2017-07-26 16:57:06 -04:00
Jarrod Johnson 12d0fe21cd Only show error message when errors exist 2017-07-26 16:37:09 -04:00
Jarrod Johnson 7a88a2825d Add 'update' to nodefirmware
nodefirmware can now execute updates on a file by file basis.
2017-07-26 15:54:21 -04:00
Jarrod Johnson bc9a498683 Provide a library for compact text node data
Some processes (e.g. nodefirmware update) warrant a different sort of
strategy to try to abbreviate, where collation falls short.  For this,
provide a columnar output that changes to line by line if stdout is not
a terminal.
2017-07-26 15:09:56 -04:00
Jarrod Johnson b5f016ad50 Provide support for removing update
This allows for cancelling and cleanup after reporting.
2017-07-26 14:10:04 -04:00
Jarrod Johnson eb3f5a8bbc Provide update status through API
Give visibility into the process.
2017-07-26 11:39:27 -04:00
Amanda Duffy 1d0b78267d Add nodebmcreset command and man page 2017-07-25 17:10:52 -04:00
Jarrod Johnson a40f015076 Phase 1 of firmware update support
Provide ability to launch firmware updates.
Next will be ability to enumerate, monitor, and
delete.
2017-07-25 17:01:32 -04:00
Amanda Duffy 4574d1bcc7 Merge remote-tracking branch 'upstream/master' 2017-07-25 15:40:06 -04:00
Jarrod Johnson 0bf21238aa Add bay number to IMM and XCC
The bay number can be opportunisticly grabbed, provide
that info in the discovery api.  In future, should add 'by-bay'
once we have enclosure data as well.
2017-07-25 12:07:18 -04:00
Jarrod Johnson 577456d999 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-20 11:05:19 -04:00
Jarrod Johnson 1dd40d36a1 Breakup logentries exceeding 65k
The data length of a log entry must not exceed 65k.  If an attempt is
made to log that much, break it up and duplicate the records.  It may make
sense to indicate a continuation explicitly, but for now just extend.
2017-07-20 11:05:06 -04:00
Jarrod Johnson ba9ea1acd8 Treat empty string same as undefined
If an administrator clears the cert fingerprint, they will
likely set it to ''.  In such a case, go down the 'no fingerprint'
path rather than reject it.
2017-07-20 10:20:22 -04:00
Jarrod Johnson 60756d9b41 Fix handling of numeric enclosure.bay
enclosure.bay is integer rather than string now.  Fix the filter
to use format, which is more robust in numeric versus string anyway.
Also, consistently make the underlying data integer rather than
sometimes string.
2017-07-20 10:12:53 -04:00
Jarrod Johnson 2795dfe7b9 Only mod noderange if bad nodes detected
This was triggering a defect in noderange. The
defect should be fixed, but in the meantime,
avoid tripping over it and looking weird anyway.
2017-07-18 15:37:37 -04:00
Jarrod Johnson a922de2ed6 Have nodeboot continue on non-failed nodes
If even one of a noderange failed to complete the
set boot device step, none would progress.  Change
the behavior so that exit code still happens and
processing does stop on timed out nodes, but go
ahead and reboot those that were fine.
2017-07-18 14:17:46 -04:00
Jarrod Johnson 5a2e4a669b Add a library for confluent version of 'xcoll'
Add a diff function with colorized intra-line diff info
2017-06-29 16:58:02 -04:00
Jarrod Johnson c2115f4df9 Fix get/set global with the stateless mode change
Since supporting stateless made the init() optional, the *_global functions
would not work if called first.  Correct this oversight.
2017-06-29 13:08:23 -04:00
Jarrod Johnson fa6650a072 Non-linux fallback for list_interface_indexes()
When errors suggest we are not in linux, make a best
effort using netifaces.
2017-06-29 10:33:10 -04:00
Jarrod Johnson 1116fed1e7 Minor style fix 2017-06-29 10:24:13 -04:00
Jarrod Johnson 0745ab0fdf Auto-encode unicode if needed
If unicode comes in to be crypted and fails, be explicit about utf-8 encoding.
2017-06-29 10:20:24 -04:00
Jarrod Johnson aeb3bd2444 Add a stateless configmanager mode
Certain embodiments of confluent may not
want to persist configuration.  Enable
an opt-in full-volatile config mode.
2017-06-29 10:17:17 -04:00
Jarrod Johnson 5dc2d8c8be Prefer SLP data on probe if available
Avoid attempting login on probe if at all possible.
2017-06-28 16:21:57 -04:00
Jarrod Johnson dc23793d32 Reorder SLP services
Make the most important one likely to be first, so that the
attributes filled out later to fixate on the most relevant.
2017-06-28 16:20:58 -04:00
Jarrod Johnson 3bcf236744 Fix serial numbers with leading spaces
Sometimes the string may have leading spaces, remove those for sane presentation.
2017-06-28 10:13:50 -04:00
Jarrod Johnson ea9caa470b Fix unexpected error on retrieving nodegroup attributes
Detect and more specifically report error message when an unknown group
is used for nodegroup commands.
2017-06-27 15:06:59 -04:00
Jarrod Johnson a894624403 Fix python 2.6 compatibility in log
python 2.6 requires a numeric argument to {}
2017-06-27 14:48:33 -04:00
Jarrod Johnson a3c06a00bf Fix confetty behavior when TERM not set
Particularly in non-interactive ssh, TERM is not set.
Assume empty string if TERM is not set.
2017-06-27 14:04:26 -04:00
Jarrod Johnson 86ed339b48 Add localonly mode to SLP
Sometimes in a likely mismatched IP situation, some SLP things will manage to reply and slow
down.  For now in the case of mismatched IPv4 being likely, provide a mode fixated on link local.
2017-06-26 09:30:23 -04:00
Jarrod Johnson ff6e4d7ab6 Fix up slp.py for windows compatibility
python in Windows does not have a required constant
in socket, hard bake the value if not defined in the module.
2017-06-26 09:23:41 -04:00
Jarrod Johnson 8126ec3791 Implement 'memory' console.logging scheme
Provide way for VT to be maintained without log to file.
2017-06-26 09:18:30 -04:00
Jarrod Johnson 8fab1ce9eb Fix permissions on buildindex.sh 2017-06-26 08:46:22 -04:00
Jarrod Johnson 9078fb01c4 Fix missing data from 1.5.0 merge 2017-06-23 08:45:34 -04:00
Jarrod Johnson e4186841b4 Correct the stub functions to work 2017-06-22 15:50:24 -04:00
Jarrod Johnson 1526a9b92d Add functions to stub out globbing
noderanges use [], and bash can do undesirable things.
Since these commands by and large do not reference files,
or at least files likely to be globbed, disable globbing.
2017-06-22 15:42:30 -04:00
Jarrod Johnson bc1b1a9676 Update nodeshell documentation on stdout/stderr 2017-06-21 14:30:54 -04:00
Jarrod Johnson f4267e6013 Add missing files from the ThinkSystem merge 2017-06-21 14:02:59 -04:00
Jarrod Johnson e0cc67f57a Implement Lenovo Thinksystem and discovery support 2017-06-20 14:56:24 -04:00
Jarrod Johnson 6117a90372 Provide a script to summarize hangtraces files 2017-05-22 09:25:51 -04:00
Amanda Duffy 3a53b2c014 Merge remote-tracking branch 'refs/remotes/xcat2/master' 2017-05-02 15:44:56 -04:00
105 changed files with 7087 additions and 833 deletions
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This is a utility to bring xcoll (plus enhancements) to confluent
# the core engine is textgroup.py, this simply provides a CLI to use
# generically
import optparse
import os
import select
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.textgroup as tg
import confluent.client
argparser = optparse.OptionParser(usage="Usage: <other command> | %prog [options]")
argparser.add_option('-a', '--abbreviate', action='store_true',
help='Attempt to use confluent server to shorten noderanges')
argparser.add_option('-d', '--diff', action='store_true',
help='Show what differs between most common '
'output group and others')
argparser.add_option('-w', '--watch', action='store_true',
help='Show intermediate results while running')
argparser.add_option('-s', '--skipcommon', action='store_true',
help='Do not print most common result, only non modal '
'groups, useful when combined with -d')
argparser.add_option('-c', '--count', action='store_true',
help='Also display count of nodes in a given group')
argparser.add_option('-r', '--reverse', action='store_true',
help='Reverse sort order to show biggest output group '
'last')
(options, args) = argparser.parse_args()
if sys.stdin.isatty():
argparser.print_help()
sys.exit(1)
grouped = tg.GroupedData()
if options.abbreviate:
grouped = tg.GroupedData(confluent.client.Command())
else:
grouped = tg.GroupedData()
def print_current():
if options.diff:
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
reverse=options.reverse)
else:
grouped.print_all(skipmodal=options.skipcommon,
count=options.count,
reverse=options.reverse)
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
grouped.add_line(*line.split(': ', 1))
if options.watch:
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 printpending:
if clearpending:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
+68 -31
View File
@@ -76,6 +76,7 @@ import confluent.tlvdata as tlvdata
import confluent.client as client
conserversequence = '\x05c' # ctrl-e, c
clearpowermessage = False
oldtcattr = None
fd = sys.stdin
@@ -115,6 +116,7 @@ def print_help():
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
status = consolename
info = []
for statekey in stateinfo:
@@ -129,6 +131,15 @@ def updatestatus(stateinfo={}):
# error will be repeated if relevant
# avoid keeping it around as stale
del laststate['error']
if 'state' in stateinfo: # currently only read power means anything
newpowerstate = stateinfo['state']['value']
if newpowerstate != powerstate and newpowerstate == 'off':
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
clearpowermessage = True
if newpowerstate == 'on' and clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
powerstate = newpowerstate
if 'clientcount' in laststate and laststate['clientcount'] != 1:
info.append('clients: %d' % laststate['clientcount'])
if 'bufferage' in stateinfo and stateinfo['bufferage'] is not None:
@@ -143,7 +154,7 @@ def updatestatus(stateinfo={}):
info.append(time.strftime('%H:%M', time.localtime(showtime)))
if info:
status += ' [' + ','.join(info) + ']'
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;console: %s\x07' % status)
sys.stdout.flush()
@@ -175,7 +186,7 @@ def recurse_format(datum, levels=0):
def prompt():
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
try:
return raw_input(target + ' -> ')
@@ -284,14 +295,17 @@ def print_result(res):
if 'databynode' in res:
print_result(res['databynode'])
return
for key in res.iterkeys():
for key in sorted(res):
notes = []
if res[key] is None:
attrstr = '%s=""' % key
elif type(res[key]) == list:
attrstr = '%s=%s' % (key, recurse_format(res[key]))
elif not isinstance(res[key], dict):
print '{0}: {1}'.format(key, res[key])
try:
print '{0}: {1}'.format(key, res[key])
except UnicodeEncodeError:
print '{0}: {1}'.format(key, repr(res[key]))
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
@@ -337,7 +351,7 @@ def do_command(command, server):
return
argv[0] = argv[0].lower()
if argv[0] == 'exit':
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(0)
elif argv[0] in ('help', '?'):
@@ -460,8 +474,11 @@ def createresource(args):
if keydata is None:
return
targpath = fullpath_target(resname)
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
if targpath.startswith('/noderange//'):
collection = targpath
else:
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
makecall(session.create, (collection, keydata))
@@ -583,7 +600,7 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
# Request default color scheme, to undo potential weirdness of terminal
sys.stdout.write('\x1b[m')
if fullexit:
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(code)
else:
@@ -709,8 +726,23 @@ def conserver_command(filehandle, localcommand):
else:
print("Unknown power state.]\r")
#check_power_state()
check_power_state()
elif localcommand[0] == 'r':
sys.stdout.write('\x1b7\x1b[999;999H\x1b[6n')
sys.stdout.flush()
reply = ''
while 'R' not in reply:
try:
reply += sys.stdin.read(1)
except IOError:
pass
reply = reply.replace('\x1b[', '')
reply = reply.replace('R', '')
height, width = reply.split(';')
sys.stdout.write('\x1b8')
sys.stdout.flush()
print('sending stty commands]')
return 'stty columns {0}\rstty rows {1}\r'.format(width, height)
elif localcommand[0] == '?':
print("help]\r")
print(". exit console\r")
@@ -721,6 +753,7 @@ def conserver_command(filehandle, localcommand):
print("pbs boot to setup\r")
print("pbn boot to network\r")
print("pb<ent> boot to default\r")
print("r send stty command to resize terminal\r")
print("<cr> abort command\r")
elif localcommand[0] == '\x0d':
print("ignored]\r")
@@ -730,7 +763,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
@@ -743,7 +779,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()
@@ -824,27 +863,20 @@ powertime = None
def check_power_state():
global powerstate, powertime
for rsp in session.read('/nodes/' + consolename + '/power/state'):
if type(rsp) == dict and 'state' in rsp:
newpowerstate = rsp['state']['value']
powertime = time.time()
if newpowerstate != powerstate and newpowerstate == 'off':
sys.stdout.write("\x1b[2J\x1b[;H[powered off]\r\n")
powerstate = newpowerstate
elif type(rsp) == dict and '_requestdone' in rsp:
break
elif type(rsp) == dict:
updatestatus(rsp)
else:
sys.stdout.write(rsp)
sys.stdout.flush()
tlvdata.send(
session.connection,
{'operation': 'retrieve',
'path': '/nodes/' + consolename + '/power/state'})
return
while inconsole or not doexit:
if inconsole:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 60)
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
@@ -860,6 +892,9 @@ while inconsole or not doexit:
updatestatus(data)
continue
if data is not None:
if clearpowermessage:
sys.stdout.write("\x1b[2J\x1b[;H")
clearpowermessage = False
try:
sys.stdout.write(data)
except IOError: # Some times circumstances are bad
@@ -902,8 +937,10 @@ while inconsole or not doexit:
tlvdata.send(session.connection, myinput)
except IOError:
pass
#if powerstate is None or powertime < time.time() - 60: # Check powerstate every 60 seconds
# check_power_state()
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
powertime = time.time()
powerstate = True
check_power_state()
else:
currcommand = prompt()
try:
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# This will take a given directory and make a 'big floppy image'
# out of it, suitable for nodemedia upload.
if [ -z "$1" -o -z "$2" ]; then
echo "Usage: $0 <directory> <imagefile>"
exit 1
fi
# Get the needed payload side
SIZE=$(du -sB 512 $1|awk '{print $1}')
ENTRIES=$(find $1 |wc -l)
# Also, each file and directory has overhead, pad size by 32kb per file,
# which should be overkill but other values proved to be inadequate
# A deeper understanding of VFAT would probably allow for more precise value
SIZE=$((SIZE + ENTRIES * 64))
dd if=/dev/zero of=$2 bs=512 count=$SIZE
# Make a big single sided floppy with many many tracks, saves on complex math
mformat -i $2 -d 1 -t $SIZE -s 1 -h 1 ::
mcopy -i $2 -s $1/* ::
+11 -2
View File
@@ -35,12 +35,16 @@ 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,...
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear attributes')
(options, args) = argparser.parse_args()
@@ -55,6 +59,7 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
@@ -62,7 +67,11 @@ exitcode = 0
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear:
if "=" in args[1] or options.clear or options.environment:
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)
try:
# setting user output to what the user inputs
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
errorNodes = set([])
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": BMC Reset Successful"
sys.exit(success)
+8 -6
View File
@@ -49,6 +49,7 @@ except IndexError:
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
sys.argv[0]))
sys.exit(1)
client.check_globbing(noderange)
bootdev = None
if len(sys.argv) > 2:
bootdev = sys.argv[2]
@@ -61,11 +62,12 @@ if options.biosmode:
else:
bootmode = 'uefi'
errnodes = set([])
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist)
if rc:
sys.exit(rc)
else:
sys.exit(session.simple_noderange_command(noderange, '/power/state', 'boot'))
persistent=options.persist,
errnodes=errnodes)
if errnodes:
noderange = noderange + ',-(' + ','.join(errnodes) + ')'
rc |= session.simple_noderange_command(noderange, '/power/state', 'boot')
sys.exit(rc)
+215
View File
@@ -0,0 +1,215 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import signal
import optparse
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
class NullOpt(object):
blame = None
clear = None
def bailout(msg, code=1):
sys.stderr.write(msg + '\n')
sys.exit(code)
argparser = optparse.OptionParser()
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
action='store_true', default=False,
help='Compare given settings to default or list settings '
'that are non default')
argparser.add_option('-d', '--detail', dest='detail',
action='store_true', default=False,
help='Provide verbose information as available, such as '
'help text and possible valid values')
argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
'examine, compare, or restore default')
(options, args) = argparser.parse_args()
cfgpaths = {
'bmc.ipv4_address': (
'configuration/management_controller/net_interfaces/management',
'ipv4_address'),
'bmc.ipv4_method': (
'configuration/management_controller/net_interfaces/management',
'ipv4_configuration'),
'bmc.ipv4_gateway': (
'configuration/management_controller/net_interfaces/management',
'ipv4_gateway'),
'bmc.hostname': (
'configuration/management_controller/hostname', 'hostname'),
}
autodeps = {
'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),)
}
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
setmode = None
assignment = {}
queryparms = {}
printsys = []
setsys = {}
forceset = False
needval = None
if len(args) == 1 or options.exclude:
if not options.exclude:
printsys = 'all'
for candidate in cfgpaths:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
def _assign_value():
if key not in cfgpaths:
setsys[key] = value
for depkey, depval in autodeps.get(key, []):
assignment[depkey] = depval
assignment[key] = value
for param in args[1:]:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
setmode = True
forceset = True
continue
if needval:
key = needval
value = param
_assign_value()
continue
if '=' in param or param[-1] == ':' or forceset:
if setmode is None:
setmode = True
if setmode != True:
bailout('Cannot do set and query in same command')
if '=' in param:
key, _, value = param.partition('=')
_assign_value()
elif param[-1] == ':':
needval = param[:-1]
else:
needval = param
else:
if setmode is None:
setmode = False
if setmode != False:
bailout('Cannot do set and query in same command')
if '.' not in param:
matchedparms = False
for candidate in cfgpaths:
if candidate.startswith('{0}.'.format(param)):
matchedparms = True
if not options.exclude:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
else:
try:
del queryparms[path]
except KeyError:
pass
if not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = param
session = client.Command()
rcode = 0
if setmode:
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
path = 'configuration/system/all'
attrib = key
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
rcode |= client.printerror(fr)
for node in fr.get('databynode', []):
r = fr['databynode'][node]
rcode |= client.printerror(r, node)
if 'value' not in r:
continue
keyval = r['value']
key, val = keyval.split('=')
if key in attrnamebypath[path]:
key = attrnamebypath[path][key]
print('{0}: {1}: {2}'.format(node, key, val))
else:
for path in queryparms:
if options.comparedefault:
continue
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)
rcode = client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import csv
import optparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
tabformat = '{0:>15}|{1:>15}|{2:>15}|{3:>36}|{4:>17}|{5:>12}|{6:>48}'
columns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
delimit = ['-' * 15, '-' * 15, '-' * 15, '-' * 36, '-' * 17, '-' * 12,
'-' * 48]
def dumpmacs(procinfo):
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
def print_disco(options, session, currmac):
procinfo = {}
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
record = [procinfo['nodename'], procinfo['modelnumber'],
procinfo['serialnumber'], procinfo['uuid'], dumpmacs(procinfo),
','.join(procinfo['types']),
','.join(sorted(procinfo['ipaddrs']))]
if options.csv:
csv.writer(sys.stdout).writerow(record)
else:
print(tabformat.format(*record))
def process_header(header):
# normalize likely header titles
fields = []
broken = False
for datum in header:
datum = datum.lower()
if datum.startswith('node') or datum.startswith('name'):
fields.append('node')
elif datum in ('nodegroup', 'nodegroups', 'group', 'groups'):
fields.append('groups')
elif datum.startswith('mac') or datum.startswith('ether'):
fields.append('mac')
elif datum.startswith('serial') or datum in ('sn', 's/n'):
fields.append('serial')
elif datum == 'uuid':
fields.append('uuid')
elif datum in ('bmc', 'imm', 'xcc'):
fields.append('hardwaremanagement.manager')
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
fields.append('net.bmc.gateway')
elif datum in ('bmcuser', 'username', 'user'):
fields.append('secret.hardwaremanagementuser')
elif datum in ('bmcpass', 'password', 'pass'):
fields.append('secret.hardwaremanagementpassword')
else:
print("Unrecognized column name {0}".format(datum))
broken = True
if broken:
sys.exit(1)
return tuple(fields)
def datum_complete(datum):
if 'node' not in datum or not datum['node']:
sys.stderr.write('Nodename is a required field')
return False
provided = set(datum)
required = set(['serial', 'uuid', 'mac'])
for field in provided & required:
if datum[field]:
break
else:
sys.stderr.write('One of the fields "Serial Number", "UUID", or '
'"MAC Address" must be provided')
return False
return True
searchkeys = set(['mac', 'serial', 'uuid'])
def search_record(datum, options, session):
for searchkey in searchkeys:
options.__dict__[searchkey] = None
for searchkey in searchkeys & set(datum):
options.__dict__[searchkey] = datum[searchkey]
return list(list_matching_macs(options, session))
def datum_to_attrib(datum):
for key in ('serial', 'uuid', 'mac'):
try:
del datum[key]
except KeyError:
pass
datum['name'] = datum['node']
del datum['node']
return datum
def import_csv(options, session):
nodedata = []
with open(options.importfile, 'r') as datasrc:
records = csv.reader(datasrc)
fields = process_header(next(records))
for record in records:
currfields = list(fields)
nodedatum = {}
for datum in record:
nodedatum[currfields.pop(0)] = datum
if not datum_complete(nodedatum):
sys.exit(1)
if not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
sys.exit(1)
nodedata.append(nodedatum)
for datum in nodedata:
maclist = search_record(datum, options, session)
datum = datum_to_attrib(datum)
nodename = datum['name']
for res in session.create('/nodes/', datum):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
continue
elif 'created' in res:
print('Defined ' + res['created'])
else:
print(repr(res))
for mac in maclist:
for res in session.update('/discovery/by-mac/{0}'.format(mac),
{'node': nodename}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
continue
elif 'assigned' in res:
print('Discovered ' + res['assigned'])
else:
print(repr(res))
def list_discovery(options, session):
if options.csv:
csv.writer(sys.stdout).writerow(columns)
else:
print(tabformat.format(*columns))
print(tabformat.format(*delimit))
for mac in list_matching_macs(options, session):
print_disco(options, session, mac)
def clear_discovery(options, session):
for mac in list_matching_macs(options, session):
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
if 'deleted' in res:
print('Cleared info for {0}'.format(res['deleted']))
else:
print(repr(res))
def list_matching_macs(options, session):
path = '/discovery/'
if options.model:
path += 'by-model/{0}/'.format(options.model)
if options.serial:
path += 'by-serial/{0}/'.format(options.serial)
if options.uuid:
path += 'by-uuid/{0}/'.format(options.uuid)
if options.type:
path += 'by-type/{0}/'.format(options.type)
if options.state:
if options.state == 'unknown':
options.state = 'unidentified'
path += 'by-state/{0}/'.format(options.state).lower()
if options.mac:
path += 'by-mac/{0}'.format(options.mac)
result = list(session.read(path))[0]
if 'error' in result:
return []
return [options.mac.replace(':', '-')]
else:
path += 'by-mac/'
return [x['item']['href'] for x in session.read(path)]
def assign_discovery(options, session):
abort = False
if options.importfile:
return import_csv(options, session)
if not (options.serial or options.uuid or options.mac):
sys.stderr.write(
"UUID (-u), serial (-s), or ether address (-e) required for "
"assignment\n")
abort = True
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if abort:
sys.exit(1)
matches = list_matching_macs(options, session)
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
{'node': options.node}):
if 'assigned' in res:
print('Assigned: {0}'.format(res['assigned']))
else:
print(repr(res))
def main():
parser = optparse.OptionParser(
usage='Usage: %prog [list|assign|rescan|clear] [options]')
# -a for 'address' maybe?
# order by
# show state (discovered or..
# nodediscover approve?
# flush to clear old data out? (e.g. no good way to age pxe data)
# also delete discovery datum... more targeted
# defect: -t lenovo-imm returns all
parser.add_option('-m', '--model', dest='model',
help='Operate with nodes matching the specified model '
'number', metavar='MODEL')
parser.add_option('-d', '--discoverystate', dest='state',
help='The discovery state of the entries (discovered, '
'identified, and unidentified)')
parser.add_option('-s', '--serial', dest='serial',
help='Operate against the system matching the specified '
'serial number', metavar='SERIAL')
parser.add_option('-u', '--uuid', dest='uuid',
help='Operate against the system matching the specified '
'UUID', metavar='UUID')
parser.add_option('-n', '--node', help='Operate with the given nodename')
parser.add_option('-e', '--ethaddr', dest='mac',
help='Operate against the system with the specified MAC '
'address', metavar='MAC')
parser.add_option('-t', '--type', dest='type',
help='Operate against the system of the specified type',
metavar='TYPE')
parser.add_option('-c', '--csv', dest='csv',
help='Use CSV formatted output', action='store_true')
parser.add_option('-i', '--import', dest='importfile',
help='Import bulk assignment data from given CSV file',
metavar='IMPORT.CSV')
(options, args) = parser.parse_args()
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
parser.print_help()
sys.exit(1)
session = client.Command()
if args[0] == 'list':
list_discovery(options, session)
if args[0] == 'clear':
clear_discovery(options, session)
if args[0] == 'assign':
assign_discovery(options, session)
if args[0] == 'rescan':
list(session.update('/discovery/rescan', {'rescan': 'start'}))
print("Rescan initiated")
if __name__ == '__main__':
main()
+10 -1
View File
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
from datetime import datetime as dt
import optparse
import os
@@ -32,6 +33,8 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [clear]")
(options, args) = argparser.parse_args()
@@ -40,11 +43,17 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
deletemode = False
if len(sys.argv) > 3:
argparser.print_help()
sys.exit(1)
if len(sys.argv) == 3:
if sys.argv[2] == 'clear':
deletemode = True
else:
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
+95 -26
View File
@@ -19,6 +19,7 @@ import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -30,22 +31,10 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
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:
@@ -63,28 +52,108 @@ def printfirm(node, prefix, data):
version = ' '.join(builddesc)
print('{0}: {1}: {2}'.format(node, prefix, version))
components = ['all']
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [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()
upfile = None
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'update':
upfile = args[2]
else:
components = []
for arg in args[1:]:
components += arg.split(',')
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
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 update_firmware(session, filename):
global exitcode
output = sq.ScreenPrinter(noderange, session)
nodeurls = {}
filename = os.path.abspath(filename)
resource = '/noderange/{0}/inventory/firmware/updates/active'.format(
noderange)
upargs = {'filename': filename}
if options.backup:
upargs['bank'] = 'backup'
noderrs = {}
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 updating ({0})!\n'.format(allerrnodes))
for node in noderrs:
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)):
exitcode |= client.printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
exitcode |= client.printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
for prefix in inv:
firmware_shown = True
printfirm(node, prefix, inv[prefix])
if not firmware_shown:
argparser.print_help()
try:
session = client.Command()
for res in session.read('/noderange/{0}/inventory/firmware/all/all'.format(
noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
for prefix in inv:
printfirm(node, prefix, inv[prefix])
if upfile is None:
show_firmware(session)
else:
update_firmware(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+11 -2
View File
@@ -36,11 +36,15 @@ import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog [options] \
\n %prog [options] nodegroup [list of attributes] \
\n %prog [options] nodegroup group=value1,value2 \
\n %prog [options] nodegroup group=value1,value2
\n %prog [options] nodegroup nodes=value1,value2 \
\n %prog -e nodegroup <attribute names to set> \
\n %prog [options] nodegroup nodes=value1,value2
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear variables')
(options, args) = argparser.parse_args()
@@ -56,6 +60,7 @@ try:
nodelist = '/{0}/{1}/'.format(nodetype,nodegroups)
except IndexError:
nodelist = '/nodegroups/'
client.check_globbing(nodegroups)
session = client.Command()
exitcode = 0
@@ -63,6 +68,10 @@ exitcode = 0
if len(args) > 1:
showtype = 'all'
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, nodegroups, options)
try:
# setting user output to what the user inputs
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=', 1)
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange
\n ''')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for r in session.delete('/nodegroups/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+1
View File
@@ -41,6 +41,7 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def main():
+3 -3
View File
@@ -38,13 +38,13 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
identifystate = None
if len(sys.argv) > 2:
identifystate = sys.argv[2]
else:
argparser.print_help()
sys.exit(1)
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
+67 -10
View File
@@ -15,8 +15,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import json
import optparse
import os
import re
import signal
import sys
@@ -31,6 +34,10 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
def pretty(text):
if text == 'pcislot':
@@ -40,14 +47,17 @@ def pretty(text):
return text
def print_mem_info(node, prefix, meminfo):
capacity = meminfo['capacity_mb'] / 1024
memdescfmt = '{0}GB PC'
if meminfo['memory_type'] == 'DDR3 SDRAM':
memdescfmt += '3-{1} '
elif meminfo['memory_type'] == 'DDR4 SDRAM':
memdescfmt += '4-{1} '
else:
print('{0}: {1}: Unrecognized Memory'.format(node, prefix))
return
if meminfo['ecc']:
memdescfmt += 'ECC '
capacity = meminfo['capacity_mb'] / 1024
memdescfmt += meminfo['module_type']
memdesc = memdescfmt.format(capacity, meminfo['speed'])
print('{0}: {1} description: {2}'.format(node, prefix, memdesc))
@@ -76,19 +86,40 @@ def printerror(res, node=None):
exitcode = 1
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
url = '/noderange/{0}/inventory/hardware/all/all'
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)
if len(args) > 1 and args[1] == 'firm':
os.execlp('nodefirmware', 'nodefirmware', noderange)
client.check_globbing(noderange)
if len(args) > 1:
if args[1] == 'firm':
os.execlp('nodefirmware', 'nodefirmware', noderange)
else:
url = '/noderange/{0}/inventory/hardware/all/system'
for arg in args:
for arg in arg.split(','):
if arg == 'serial':
filters.append(re.compile('serial number'))
elif arg == 'model':
filters.append(re.compile('^model'))
filters.append(re.compile('product name'))
elif arg == 'uuid':
filters.append(re.compile('uuid'))
elif arg == 'mac':
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('/noderange/{0}/inventory/hardware/all/all'.format(
noderange)):
for res in session.read(url.format(noderange)):
printerror(res)
if 'databynode' not in res:
continue
@@ -99,7 +130,13 @@ try:
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
if not inv['present']:
print '{0}: {1}: Not Present'.format(node, prefix)
if not filters:
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)
@@ -107,14 +144,34 @@ try:
info.pop('chassis_extra', None)
info.pop('product_extra', None)
if 'memory_type' in info:
print_mem_info(node, prefix, 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:
if filters:
for filter in filters:
if filter.match(datum.lower()):
break
else:
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)
+1
View File
@@ -47,6 +47,7 @@ def main():
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
nodelist = '/nodes/'
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
showtype='all'
+190
View File
@@ -0,0 +1,190 @@
#!/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 attach_media(noderange, media):
global exitcode
session = client.Command()
if ':' not in media:
sys.stderr.write('Full URL required for attach\n')
sys.exit(1)
resource = '/noderange/{0}/media/attach'.format(noderange)
for res in session.update(resource, {'url': media}):
printerror(res)
list_media(noderange, media)
def list_media(noderange, media):
session = client.Command()
resource = '/noderange/{0}/media/current'.format(noderange)
for res in session.read(resource):
printerror(res)
for node in res.get('databynode', []):
url = res['databynode'][node].get('url', None)
name = res['databynode'][node].get('name', None)
if url and not res['databynode'][node].get('secure', False):
name += ' (insecure)'
if not name:
continue
print('{0}: {1}'.format(node, url + '/' + name if url else name))
def detach_media(noderange, media):
global exitcode
session = client.Command()
resource = '/noderange/{0}/media/detach'.format(noderange)
for res in session.update(resource, {'detachall': 1}):
printerror(res)
def upload_media(noderange, media):
global exitcode
if not os.path.exists(media):
sys.stderr.write('Unable to locate requested file {0}\n'.format(
media))
sys.exit(404)
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/media/uploads/'.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 receiving media ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
list_media(noderange, media)
funmap = {
'upload': upload_media,
'attach': attach_media,
'detachall': detach_media,
'list': list_media,
}
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> [list|upload|attach|detachall] "
"<filename>|all|<url>",
epilog='\nupload will take the specified file and upload it to the '
'BMC.\n\n'
'attach will instruct the BMC to connect a remote media to the '
'specified url.\n\ndetachall will remove *ALL* uploaded and '
'attached urls from the BMC\n\nlist shows currently mounted '
'media.\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 in ('attach', 'upload'):
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()
+23 -6
View File
@@ -34,19 +34,22 @@ import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange "
"([status|on|off|shutdown|boot|reset])")
argparser.add_option('-p', '--showprevious', dest='previous',
action='store_true', default=False,
help='Show previous power state')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
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()
@@ -54,5 +57,19 @@ if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
session = client.Command()
exitcode = 0
session.add_precede_key('oldstate')
sys.exit(
session.simple_noderange_command(noderange, '/power/state', setstate))
if options.previous:
# get previous states
prev = {}
for rsp in session.read("/noderange/{0}/power/state".format(noderange)):
# gets previous (current) states
databynode = rsp["databynode"]
for node in databynode:
prev[node] = databynode[node]["state"]["value"]
# add dictionary to session
session.add_precede_dict(prev)
sys.exit(session.simple_noderange_command(noderange, '/power/state', setstate))
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange
\n ''')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
errorNodes = set([])
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": Reseat successful"
sys.exit(success)
+42 -23
View File
@@ -34,21 +34,25 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def run():
concurrentprocs = 168
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser = optparse.OptionParser(
usage="Usage: %prog node commandexpression",
usage="Usage: %prog noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
concurrentprocs = options.count
client.check_globbing(args[0])
c = client.Command()
cmdstr = " ".join(args[1:])
@@ -56,10 +60,14 @@ def run():
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
ex = exp['databynode']
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
@@ -68,28 +76,39 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
exitcode = 0
if not all or exitcode:
sys.exit(exitcode)
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
data = r.readline()
desc = pipedesc[r]
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
sys.stderr.write('{0}: {1}'.format(node, data))
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
@@ -107,4 +126,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
if __name__ == '__main__':
run()
run()
+5 -1
View File
@@ -36,6 +36,7 @@ import confluent.client as client
sensorcollections = {
'all': 'sensors/hardware/all/all',
'energy': 'sensors/hardware/energy/all',
'temperature': 'sensors/hardware/temperature/all',
'temp': 'sensors/hardware/temperature/all',
'power': 'sensors/hardware/power/all',
@@ -64,6 +65,7 @@ if options.numreadings:
options.interval = 1
try:
noderange = args[0]
client.check_globbing(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
@@ -78,7 +80,6 @@ for sensorgroup in args[1:]:
sensors.append('sensors/hardware/all/' + sensor)
if not sensors:
sensors = ['sensors/hardware/all/all']
session = client.Command()
exitcode = 0
sensorheaders = {}
@@ -125,6 +126,8 @@ def sensorpass(showout=True, appendtime=False):
if showout:
if sensedata['value'] is None:
showval = ''
elif isinstance(sensedata['value'], float):
showval = u' {0:.5f} '.format(sensedata['value'])
else:
showval = u' {0} '.format(sensedata['value'])
if sensedata['units'] not in (None, u''):
@@ -146,6 +149,7 @@ def sensorpass(showout=True, appendtime=False):
'%Y-%m-%dT%H:%M:%S')
print(u'{0}: {1}:{2}'.format(
node, sensedata['name'], showval).encode('utf-8'))
sys.stdout.flush()
return resultdata
+1
View File
@@ -48,6 +48,7 @@ argparser.add_option('-u', '--uefi', dest='uefi', action='store_true',
try:
noderange = args[0]
client.check_globbing(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
+45 -25
View File
@@ -19,7 +19,6 @@ from collections import deque
import optparse
import os
import select
import shlex
import signal
import subprocess
import sys
@@ -34,21 +33,26 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def run():
concurrentprocs = 168
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser = optparse.OptionParser(
usage="Usage: %prog node commandexpression",
usage="Usage: %prog noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
cmdstr = " ".join(args[1:])
@@ -56,40 +60,56 @@ def run():
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
ex = exp['databynode']
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = ['ssh', node] + shlex.split(cmd)
cmdv = ['ssh', node, cmd]
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
exitcode = 0
if not all or exitcode:
sys.exit(exitcode)
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
data = r.readline()
desc = pipedesc[r]
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
node = desc['node']
data = True
while data and select.select([r], [], [], 0)[0]:
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
sys.stderr.write('{0}: {1}'.format(node, data))
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
@@ -107,4 +127,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
if __name__ == '__main__':
run()
run()
+205 -46
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import anydbm as dbm
import errno
import hashlib
import os
import shlex
import socket
import ssl
import sys
@@ -26,6 +27,30 @@ import confluent.tlvdata as tlvdata
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'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()
def _parseserver(string):
if ']:' in string:
@@ -43,8 +68,10 @@ def _parseserver(string):
class Command(object):
def __init__(self, server=None):
self._prevdict = None
self._prevkeyname = None
self.connection = None
self._currnoderange = None
if server is None:
if 'CONFLUENT_HOST' in os.environ:
self.serverloc = os.environ['CONFLUENT_HOST']
@@ -77,8 +104,13 @@ class Command(object):
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def handle_results(self, ikey, rc, res):
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None):
if 'error' in res:
if errnodes is not None:
errnodes.add(self._currnoderange)
sys.stderr.write('Error: {0}\n'.format(res['error']))
if 'errorcode' in res:
return res['errorcode']
@@ -89,6 +121,8 @@ class Command(object):
res = res['databynode']
for node in res:
if 'error' in res[node]:
if errnodes is not None:
errnodes.add(node)
sys.stderr.write('{0}: Error: {1}\n'.format(
node, res[node]['error']))
if 'errorcode' in res[node]:
@@ -103,15 +137,19 @@ class Command(object):
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
print('{0}: {2}->{1}'.format(
cprint('{0}: {2}->{1}'.format(
node, val, res[node][self._prevkeyname]['value']))
elif self._prevdict and node in self._prevdict:
cprint('{0}: {2}->{1}'.format(
node, val, self._prevdict[node]))
else:
print('{0}: {1}'.format(node, val))
cprint('{0}: {1}'.format(node, val))
return rc
def simple_noderange_command(self, noderange, resource, input=None,
key=None, **kwargs):
key=None, errnodes=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
@@ -123,15 +161,16 @@ class Command(object):
if input is None:
for res in self.read('/noderange/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
rc = self.handle_results(ikey, rc, res, errnodes)
else:
kwargs[ikey] = input
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
rc = self.handle_results(ikey, rc, res, errnodes)
self._currnoderange = None
return rc
except KeyboardInterrupt:
print('')
cprint('')
return 0
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
@@ -155,7 +194,7 @@ class Command(object):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
print('')
cprint('')
return 0
def read(self, path, parameters=None):
@@ -222,8 +261,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()
@@ -237,7 +275,7 @@ class Command(object):
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
print 'Adding new key for %s:%s' % (server, port)
cprint('Adding new key for %s:%s' % (server, port))
khf[hostid] = fingerprint
@@ -271,42 +309,65 @@ def attrrequested(attr, attrlist, seenattributes):
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate == attr:
if candidate in _attraliases:
candidate = _attraliases[candidate]
if candidate.lower() == attr.lower():
seenattributes.add(truename)
return True
elif attr.startswith(candidate + '.'):
elif attr.lower().startswith(candidate.lower() + '.'):
seenattributes.add(truename)
return True
return False
def printattributes(session, requestargs, showtype, nodetype, noderange, options):
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return print_attrib_path(path, session, requestargs, options)
def print_attrib_path(path, session, requestargs, options, rename=None):
exitcode = 0
seenattributes = set([])
for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
for res in session.read(path):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in res['databynode']:
for attr in res['databynode'][node]:
for node in sorted(res['databynode']):
for attr, val in sorted(
res['databynode'][node].items(),
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
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)
else:
printattr = attr
currattr = res['databynode'][node][attr]
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
if show_attr(attr, requestargs, seenattributes, options):
if 'value' in currattr:
if currattr['value'] is not None:
val = currattr['value']
if isinstance(val, list):
val = ','.join(val)
attrout = '{0}: {1}: {2}'.format(
node, attr, currattr['value'])
node, printattr, val)
else:
attrout = '{0}: {1}:'.format(node, attr)
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node, attr)
attrout = '{0}: {1}: ********'.format(node,
printattr)
else:
attrout = '{0}: {1}:'.format(node, attr)
attrout = '{0}: {1}:'.format(node, printattr)
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, attr,
'{2}'.format(node, printattr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
@@ -314,11 +375,14 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, dictout)))
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
else:
print ("CODE ERROR" + repr(attr))
if options.blame or 'broken' in currattr:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or 'broken' in currattr:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
@@ -330,7 +394,36 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
print attrout
try:
comparedefault = options.comparedefault
except AttributeError:
comparedefault = False
if comparedefault:
try:
exclude = options.exclude
except AttributeError:
exclude = False
if ((requestargs and not exclude) or
(currattr.get('default', None) is not None and
currattr.get('value', None) is not None and
currattr['value'] != currattr['default'])):
cprint('{0}: {1}: {2} (Default: {3})'.format(
node, printattr, currattr['value'],
currattr['default']))
else:
try:
details = options.detail
except AttributeError:
details = False
if details:
if currattr.get('help', None):
attrout += ' (Help: {0})'.format(
currattr['help'].encode('utf-8'))
if currattr.get('possible', None):
attrout += ' (Choices: {0})'.format(
','.join(currattr['possible']))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
@@ -340,6 +433,19 @@ def printattributes(session, requestargs, showtype, nodetype, noderange, options
return exitcode
def show_attr(attr, requestargs, seenattributes, options):
try:
reverse = options.exclude
except AttributeError:
reverse = False
if requestargs is None or requestargs == []:
return True
processattr = attrrequested(attr, requestargs, seenattributes)
if reverse:
processattr = not processattr
return processattr
def printgroupattributes(session, requestargs, showtype, nodetype, noderange, options):
exitcode = 0
seenattributes = set([])
@@ -377,8 +483,8 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, dictout)))
else:
print ("CODE ERROR" + repr(attr))
print attrout
cprint("CODE ERROR" + repr(attr))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
@@ -401,28 +507,81 @@ def updateattrib(session, updateargs, nodetype, noderange, options):
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange,
'attributes/all',
value, key)
else:
exitcode = session.simple_noderange_command(noderange,
'attributes/all',
value, key)
sys.exit(exitcode)
else:
if "=" in updateargs[1]:
try:
if len(updateargs[1:]) > 1:
for val in updateargs[1:]:
val = val.split('=')
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all'.format(noderange),
val[1],val[0])
else:
exitcode = session.simple_noderange_command(noderange, 'attributes/all'.format(noderange),
val[1], val[0])
else:
val = updateargs[1].split('=')
if nodetype == "nodegroups" :
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all'.format(noderange),
val[1], val[0])
for val in updateargs[1:]:
val = val.split('=', 1)
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
value = {'prepend': val[1]}
elif val[0][-1] in ('-', '^'):
value = {'remove': val[1]}
else:
exitcode = session.simple_noderange_command(noderange, 'attributes/all'.format(noderange),
val[1], val[0])
key = val[0]
value = val[1]
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all',
value, key)
else:
exitcode = session.simple_noderange_command(noderange, 'attributes/all',
value, key)
except:
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
exitcode = 1
sys.exit(exitcode)
return exitcode
# So we try to prevent bad things from happening when globbing
# We tried to head this off at the shell, but the various solutions would end
# up breaking the shell in various ways (breaking pipe capability if using
# DEBUG, breaking globbing if in pipe, etc)
# Then we tried to parse the original commandline instead, however shlex isn't
# going to parse full bourne language (e.g. knowing that '|' and '>' and
# a world of other things would not be in our command line
# so finally, just make sure the noderange appears verbatim in the command line
# if we glob to something, then bash will change noderange and this should
# detect it and save the user from tragedy
def check_globbing(noderange):
if not os.path.exists(noderange):
return True
rawargs = os.environ.get('CURRENT_CMDLINE', None)
if rawargs:
rawargs = shlex.split(rawargs)
for arg in rawargs:
if arg.startswith('$'):
arg = arg[1:]
if arg.endswith(';'):
arg = arg[:-1]
arg = os.environ.get(arg, '$' + arg)
if arg.startswith(noderange):
break
else:
sys.stderr.write(
'Shell glob conflict detected, specified target "{0}" '
'not in command line, but is a file. You can use "set -f" in '
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
@@ -0,0 +1,85 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import fcntl
import sys
import struct
import termios
def get_screenwidth():
return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'....'))[1]
class ScreenPrinter(object):
def __init__(self, noderange, client, textlen=4):
self.squeeze = sys.stdout.isatty()
self.textlen = textlen
self.noderange = noderange
self.client = client
self.nodeoutput = {}
self.nodelist = []
maxlen = 0
for ans in self.client.read('/noderange/{0}/nodes/'.format(noderange)):
if 'error' in ans:
sys.stderr.write(ans['error'])
continue
nodename = ans['item']['href'][:-1]
if len(nodename) > maxlen:
maxlen = len(nodename)
self.nodelist.append(nodename)
self.nodeoutput[nodename] = ''
self.nodenamelen = maxlen
self.textlen = textlen
self.fieldwidth = maxlen + textlen + 1 # 1 for column
def set_output(self, node, text):
self.nodeoutput[node] = text
if len(text) >= self.textlen:
self.textlen = len(text) + 1
self.fieldwidth = self.textlen + self.nodenamelen + 1
self.drawscreen()
def drawscreen(self):
if self.squeeze:
currwidth = get_screenwidth()
numfields = currwidth // self.fieldwidth
fieldformat = '{{0:>{0}}}:{{1:{1}}}'.format(self.nodenamelen,
self.textlen)
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
else:
numfields = 1
fieldformat = '{0}: {1}'
currfields = 0
for node in self.nodelist:
if currfields >= numfields:
sys.stdout.write('\n')
currfields = 1
else:
currfields += 1
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
sys.stdout.write('\n')
sys.stdout.flush()
if __name__ == '__main__':
import confluent.client as client
c = client.Command()
p = ScreenPrinter('n1-n13', c)
p.set_output('n3', 'Upload: 67%')
+43
View File
@@ -0,0 +1,43 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# 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 re
numregex = re.compile('([0-9]+)')
def naturalize_string(key):
"""Analyzes string in a human way to enable natural sort
:param key: The node name to analyze
:returns: A structure that can be consumed by 'sorted'
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(numregex, key)]
def natural_sort(iterable):
"""Return a sort using natural sort if possible
:param iterable:
:return:
"""
try:
return sorted(iterable, key=naturalize_string)
except TypeError:
# The natural sort attempt failed, fallback to ascii sort
return sorted(iterable)
+217
View File
@@ -0,0 +1,217 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import difflib
import re
import sys
try:
range = xrange
except NameError:
pass
numregex = re.compile('([0-9]+)')
def humanify_nodename(nodename):
"""Analyzes nodename in a human way to enable natural sort
:param nodename: The node name to analyze
:returns: A structure that can be consumed by 'sorted'
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(numregex, nodename)]
def _colorize_line(orig, mask):
highlighted = False
newline = orig[0]
for i in range(1, len(orig)):
if i > len(mask) - 1:
if highlighted:
newline += '\x1b[0m'
newline += orig[i:]
break
if highlighted and mask[i] == ' ':
highlighted = False
newline += '\x1b[0m'
elif not highlighted and mask[i] != ' ':
highlighted = True
newline += '\x1b[31m'
newline += orig[i]
newline += '\x1b[0m'
return newline
def near_diff(idx, diffdata):
maxidx = idx + 3
if maxidx > len(diffdata) -1:
maxidx = len(diffdata) - 1
idx = idx - 3
if idx < 0:
idx = 0
while idx <= maxidx:
if len(diffdata[idx]) > 0 and diffdata[idx][0] in ('+', '-'):
return True
idx += 1
return False
def colordiff(first, second):
diffdata = list(difflib.ndiff(first, second))
quiet = True
for i in range(len(diffdata)):
if i < len(diffdata) - 1 and diffdata[i + 1].startswith('?'):
if quiet:
quiet = False
yield '@@'
yield _colorize_line(diffdata[i], diffdata[i + 1])
elif not diffdata[i].startswith('?') and near_diff(i, diffdata):
if quiet:
quiet = False
yield '@@'
yield diffdata[i]
elif not diffdata[i].startswith('?'):
quiet = True
class GroupedData(object):
'''A post processor to sort and compare per-node data
:param confluentconnection: If given, will attempt to use the connection to abbreviate noderanges
'''
def __init__(self, confluentconnection=None):
self.bynode = {}
self.byoutput = {}
self.header = {}
self.client = confluentconnection
def generate_byoutput(self):
self.byoutput = {}
for n in self.bynode:
output = '\n'.join(self.bynode[n])
if output not in self.byoutput:
self.byoutput[output] = set([n])
else:
self.byoutput[output].add(n)
def add_line(self, node, line):
if node not in self.bynode:
self.bynode[node] = [line]
else:
self.bynode[node].append(line)
def get_group_text(self, nodes):
if self.client:
headerkey = ','.join(sorted(nodes))
if headerkey in self.header:
return self.header[headerkey]
noderange = ''
for reply in self.client.create('/noderange//abbreviate',
{'nodes': sorted(nodes)}):
noderange = reply['noderange']
self.header[headerkey] = noderange
return noderange
else:
return ','.join(sorted(nodes, key=humanify_nodename))
def print_all(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
self.generate_byoutput()
modaloutput = None
ismodal = True
if reverse:
outdatalist = sorted(
self.byoutput, key=lambda x: [len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))])
else:
outdatalist = sorted(
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))])
if reverse and skipmodal:
# if reversed, the last is biggest and should be skipped if modal
outdatalist = outdatalist[:-1]
for outdata in outdatalist:
if not reverse and skipmodal:
# If big first, this makes skipmodal skip first
skipmodal = False
continue
currout = '====================================\n'
currout += self.get_group_text(self.byoutput[outdata])
currout += '\n====================================\n'
if count:
currout += 'Count: {0}'.format(len(list(
self.byoutput[outdata])))
currout += '\n====================================\n'
currout += outdata
currout += '\n\n'
output.write(currout)
output.flush()
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
self.generate_byoutput()
modaloutput = None
ismodal = True
revoutput = []
for outdata in sorted(
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))]):
if modaloutput is None:
modaloutput = outdata
if skipmodal:
skipmodal = False
ismodal = False
continue
currout = '====================================\n'
currout += self.get_group_text(self.byoutput[outdata])
currout += '\n====================================\n'
if count:
currout += 'Count: {0}'.format(len(list(
self.byoutput[outdata])))
currout += '\n====================================\n'
if ismodal:
ismodal = False
currout += outdata
else:
currout += '\n'.join(colordiff(modaloutput.split('\n'),
outdata.split('\n')))
currout += '\n\n'
if reverse:
revoutput.append(currout)
else:
output.write(currout)
for currout in reversed(revoutput):
output.write(currout)
output.flush()
if __name__ == '__main__':
groupoutput = GroupedData()
for line in sys.stdin.read().split('\n'):
if not line:
continue
groupoutput.add_line(*line.split(': ', 1))
groupoutput.print_deviants()
+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)
+35
View File
@@ -2,3 +2,38 @@ PATH=/opt/confluent/bin:$PATH
export PATH
MANPATH=/opt/confluent/share/man:$MANPATH
export MANPATH
# The aliases below are to signify that file globbing is unwelcome at the shell
# this avoids a problem if a user does a noderange like 'n[21-33] and there is a file
# in the directory like 'n3' that causes the parameter to change and target a totally
# different node
# Unfortunately in bourne shell, we cannot reliably ensure a prepended set-f
# and an appended set +f are both run. alias seems to be the only mechanism
# that can intervene before glob expansion, but it lacks power.
# putting it into a function to append is all well and good, *except* that
# if doing something like 'nodepower compute|grep' causes set -f to execute
# in current shell, and the function to be in a subshell and leaves globbing
# disabled in the parent shell. Instead, store the current command in a
# variable and use that to check for misglobbed noderanges, which was the goal
alias nodeattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeattrib'
alias nodebmcreset='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodebmcreset'
alias nodeboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeboot'
alias nodeconfig='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconfig'
alias nodeconsole='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconsole'
alias nodedefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedefine'
alias nodeeventlog='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeeventlog'
alias nodefirmware='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodefirmware'
alias nodegroupattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupattrib'
alias nodegroupdefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupdefine'
alias nodegroupremove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupremove'
alias nodehealth='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodehealth'
alias nodeidentify='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeidentify'
alias nodeinventory='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeinventory'
alias nodelist='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelist'
alias nodemedia='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodemedia'
alias nodepower='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodepower'
alias noderemove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderemove'
alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodereseat'
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
+88
View File
@@ -0,0 +1,88 @@
collate(1) -- Organize text input by node
==============================
## SYNOPSIS
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
## DESCRIPTION
**collate** takes input in the form of <nodename>: <data> and groups
the output for each <nodename> together, and checks for identical data to further group nodes together.
Output groups are sorted in descending size such that the topmost group is the largest, and the
last output group is the smallest. In the event of equally sized output groups,
the groups are sorted alphanumerically by their header, and each header has
node and group names sorted alphanumerically.
## OPTIONS
* `-a`, `--abbreviate`:
Use confluent to attempt to shorten the noderange. This can help identify
when output differs along a telling group boundary. For example, if
output suggests a large number of nodes are unreachable, abbreviate
showing 'rack1' as being unreachable may make more obvious a possible cause.
* `-d`, `--diff`:
Express all but the most common result group in terms of diff from
the most common result group
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
* `-s`, `--skipcommon`:
Suppress printing of the most common result text group. This is used to
focus on stray output against a well known and expected result.
* `-c`, `--count`:
Print a count of the number of nodes in an output group under the
noderange.
* `-r`, `--reverse`:
Rather than starting with most common to least common, start with
the least common and print the most common last.
## EXAMPLES
* Organizing power state of multiple nodes:
`# nodepower n1-n12 | collate`
`====================================`
`n1,n2,n3,n4,n7,n8,n9,n10,n11,n12`
`====================================`
`on`
` `
`====================================`
`n5,n6`
`====================================`
`off`
* Using diff to detect distinct UEFI configuration
`# pasu n1-n4 show Processors|collate -d -s`
`====================================`
`n3`
`====================================`
`@@`
` Processors.ProcessorPerformanceStates=Enable`
` Processors.C-States=Enable`
` Processors.PackageACPIC-StateLimit=ACPI C3`
`- Processors.C1EnhancedMode=Enable`
`+ Processors.C1EnhancedMode=Disable`
`- Processors.Hyper-Threading=Enable`
`+ Processors.Hyper-Threading=Disable`
` Processors.ExecuteDisableBit=Enable`
` Processors.IntelVirtualizationTechnology=Enable`
` `
`====================================`
`n1`
`====================================`
`@@`
` Processors.ProcessorPerformanceStates=Enable`
` Processors.C-States=Enable`
` Processors.PackageACPIC-StateLimit=ACPI C3`
`- Processors.C1EnhancedMode=Enable`
`+ Processors.C1EnhancedMode=Disable`
` Processors.Hyper-Threading=Enable`
` Processors.ExecuteDisableBit=Enable`
+12 -2
View File
@@ -3,9 +3,10 @@ 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> ...`
## DESCRIPTION
@@ -18,9 +19,16 @@ 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>`
Note that `nodeattrib <group>` will likely not provide the expected behavior.
See nodegroupattrib(8) command on how to manage attributes on a group level.
@@ -30,6 +38,8 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
Annotate inherited and expression based attributes to show their base value.
* `-c`, `--clear`:
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
## EXAMPLES
* Listing matching nodes of a simple noderange:
@@ -39,7 +39,7 @@ number of operators at the end to indicate formatting changes.
Another common element to pull into an expression is the node name in whole:
`hardwaremanagement.manager={nodename}-imm`
`hardwaremanagement.manager={node}-imm`
Additionally other attributes may be pulled in:
@@ -47,7 +47,7 @@ Additionally other attributes may be pulled in:
Multiple expressions are permissible within a single attribute:
`hardwaremanagement.manager={nodename}-{hardwaremanagement.method}`
`hardwaremanagement.manager={node}-{hardwaremanagement.method}`
A note to developers: in general the API layer will automatically recognize a
generic set attribute to string with expression syntax and import it as an
@@ -0,0 +1,19 @@
nodebmcreset(8) -- Reset management controller
=========================================================
## SYNOPSIS
`nodebmcreset <noderange>`
## DESCRIPTION
`nodebmcreset` allows you to reset the management controller of the specified noderange
## EXAMPLES:
* Reset the management controller for nodes n1 through n4:
`# nodebmcreset n1-n4`
`n1: BMC Reset Successful`
`n2: BMC Reset Successful`
`n3: BMC Reset Successful`
`n4: BMC Reset Successful`
+50
View File
@@ -0,0 +1,50 @@
nodeconfig(8) -- Show or change node configuration
==================================================
## SYNOPSIS
`nodeconfig <noderange> [options] [<configuration>..]`
`nodeconfig <noderange> [options] [<configuration=value>..]`
## DESCRIPTION
**nodeconfig** manages the configuration of nodes managed by confluent.
Rather than manipulating the confluent database, this actually modifies the
running configuration on the node firmware. Calling without '=' will show the
current value, and '=' will change the value. Network information can be
given as a node expression, as documented in the man page for nodeattribexpressions(5).
## OPTIONS
* `-c`, `--comparedefault`:
Take the given settings and compare against default value, if available. If
no configuration values are specified, it will show only those that differ.
If combined with `-x`, will show all differing values except those indicated
by `-x`
* `-x`, `--exclude`:
Rather than listing only the specified configuration parameters, list all
attributes except for the specified ones
* `-d`, `--detail`:
Provide detailed data as available. This can include help text and valid
values for a setting.
## EXAMPLES
* Showing the current IP configuration of noderange BMC/IMM/XCC:
`# nodeconfig s3,s4 bmc`
`s3: bmc.ipv4_address: 172.30.254.193/16`
`s3: bmc.ipv4_method: DHCP`
`s3: bmc.ipv4_gateway: 172.30.0.6`
`s4: bmc.ipv4_address: 172.30.254.192/16`
`s4: bmc.ipv4_method: DHCP`
`s4: bmc.ipv4_gateway: 172.30.0.6`
* Changing nodes `s3` and `s4` to have the ip addressess 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
`# nodeconfig s3,s4 bmc.ipv4_address=10.1.2.{n1}/16`
## SEE ALSO
nodeattribexpressions(5)
@@ -11,6 +11,11 @@ text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `
then `.`. Note that console output by default is additionally logged to
`/var/log/confluent/consoles/`**NODENAME**.
When the console connection to the target is broken, then confluent on backend
will initiate an automatic retry interval that is randomized between 2 and 4 minutes.
The reopen escape sequence below requests an immediate retry, as does connecting
a new session.
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
@@ -42,6 +47,10 @@ keystroke will be interpreted as a command. The following commands are availabl
* `pbn`:
[Power Boot Network]
Request immediate boot to network
* `r`:
[send Resize]
This queries the current terminal and sends stty commands to advertise the user termineal
size to the remote console
* `?`:
Get a list of supported commands
* `<ent>`:
+28
View File
@@ -0,0 +1,28 @@
nodedefine(8) -- Define new confluent nodes
## SYNOPSIS
`nodedefine <noderange> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodedefine` allows the definition of new nodes for the confluent management
system. It has the same syntax as `nodeattrib(8)`, and the commands differ in
that `nodeattrib(8)` will error if a node does not exist.
## EXAMPLES
* Define two racks of nodes, named r{rack}u{u}:
`# nodedefine r1u1-r2u4`
`r1u4: created`
`r1u1: created`
`r1u2: created`
`r1u3: created`
`r2u4: created`
`r2u3: created`
`r2u2: created`
`r2u1: created`
## SEE ALSO
noderange(5), nodeattribexpressions(8)
+130
View File
@@ -0,0 +1,130 @@
nodediscover(8) -- List or manage confluent node discovery
=========================================================
## SYNOPSIS
`nodediscover rescan`
`nodediscover [options] list`
`nodediscover [options] assign`
`nodediscover [options] rescan`
`nodediscover [options] clear`
## DESCRIPTION
**nodediscover** provides streamlined access to the confluent discovery data
and assignment. Nodes are detected through either an active scan (as occurs
at service startup and on request by nodediscover rescan) or through passive
detection (as a target comes online, it may attempt to register with the
network).
**nodediscover list** provides the currently known data in tabular format. The
data may be filtered by various parameters, as denoted in the options below.
**nodediscover assign** performs manual discovery, assigning an entry to a node
identity or, using `-i`, using a csv file to assign nodes all at once. For
example, a spreadsheet of serial numbers to desired node names could be used.
**nodediscover rescan** requests the server to do an active sweep for new
devices. Generally every effort is made to passively detect devices as they
become available (as they boot or are plugged in), however sometimes an active
scan is the best approach to catch something that appears to be missing.
**nodedsicover clear** requests the server forget about a selection of
detected device. It takes the same arguments as **nodediscover list**.
## CSV FORMAT
The CSV format used by nodediscover consists of one header describing the
columns followed by the data. The available columns are:
* node: The name desired for the node in confluent
* groups: A comma delimited list of groups to put the node into (using normal CSV escape rules for the commas)
* mac: The mac address of the node
* serial: The serial number of the node
* uuid: The uuid of the node
* bmc: The name or ip address that should be assigned to the BMC, regardless of current address
* bmc_gateway: IP address of gateway, if desired
* bmcuser: The desired username for the BMC to have as administrator
* bmcpass: The desired password for the BMC to require
Note that node is the only mandatory field. To identify the systems, one of
mac, serial, or uuid should be specified, it is pointless to provide more than
one of these columns. Other attributes if not provided may be defined through
nodeattrib or group inherited. It is possible to define nodes without ever
providing a BMC ip, in which case IPv6 will be used automatically if possible.
One example of a valid CSV file would be:
node,serial,bmc,bmcuser,bmcpass
n1,06DPMDF,172.30.204.1,admin,Passw0rd12
n2,J30002HG,172.30.204.2,admin,Passw0rd12
Which would use the serial number to assign the name and other three values to
the nodes.
## OPTIONS
* `-m MODEL`, `--model=MODEL`:
Operate with nodes matching the specified model number
* `-s SERIAL`, `--serial=SERIAL`:
Operate against the system matching the specified
serial number
* `-u UUID`, `--uuid=UUID`:
Operate against the system matching the specified UUID
* `-n NODE`, `--node=NODE`:
Operate with the given nodename
* `-e MAC`, `--ethaddr=MAC`:
Operate against the system with the specified MAC
address
* `-t TYPE`, `--type=TYPE`:
Operate against the system of the specified type
* `-c`, `--csv`:
Use CSV formatted output
* `-i IMPORT.CSV`, `--import=IMPORT.CSV`:
Import bulk assignment data from given CSV file
* `-d STATE`, `--discoverystate=STATE`:
Indicate devices with a particular state. The states are listed below
* discovered: The device has been identified and has also had discovery
activities performed, including any relevant certificate
exchanges and deploying user and network configuration.
* identified: The device has been identified as to what node it is
supposed to be, however no active changes to the attributes
or device configuration has been performed. This is
generally discovery awaiting approval due to
discovery.policy specifying a strict security model.
* unidentified: A device has been sensed, but no node identity has been
established at all. It provides data that can be used
for nodediscover assign, as well as current IP addresses
that can be used for manual efforts as possible.
## EXAMPLES
* Listing all detected Lenovo IMMv2 systems on a local network:
`# nodediscover list -t lenovo-imm2`
` Node| Model| Serial| UUID| Mac Address| Type| Current IP Addresses`
`---------------|---------------|---------------|------------------------------------|-----------------|------------|------------------------------------------------`
` r2| 5463AC1| 06DPMDF|5f7133b8-c8cb-11e4-99a9-40f2e9b91018|40:f2:e9:b9:10:1d| lenovo-imm2| 172.30.204.1,fe80::42f2:e9ff:feb9:101d%eth1`
` | 7906AC1| 06PBX15|e98d483d-2759-11e1-8ffd-5cf3fc11249c|5c:f3:fc:11:24:9f| lenovo-imm2| 172.30.3.12,fe80::5ef3:fcff:fe11:249f%eth1`
` n1| 8737AC1| 23XXH41|14dd3ba6-5c38-11e1-931a-5cf3fc6e4680|5c:f3:fc:6e:13:e1| lenovo-imm2| 172.30.3.1,fe80::5ef3:fcff:fe6e:13e1%eth1`
` n7| 8737AC1| 23XXH32|79d2ce28-5cd5-11e1-8c86-5cf3fc6e46b0|5c:f3:fc:6e:13:f9| lenovo-imm2| 172.30.3.7,fe80::5ef3:fcff:fe6e:13f9%eth1`
` n8| 8737AC1| 23XXH49|551a8438-5cd5-11e1-8d6c-5cf3fc6e4708|5c:f3:fc:6e:14:25| lenovo-imm2| 172.30.3.8,fe80::5ef3:fcff:fe6e:1425%eth1`
` n3| 8737AC1| 23XXH30|1dd7f7b3-5da5-11e1-baf0-5cf3fc6e4738|5c:f3:fc:6e:14:3d| lenovo-imm2| 172.30.3.3,fe80::5ef3:fcff:fe6e:143d%eth1`
` n4| 8737AC1| 23XXH35|45b81dae-5d9b-11e1-8337-5cf3fc6e4858|5c:f3:fc:6e:14:cd| lenovo-imm2| 172.30.3.4,fe80::5ef3:fcff:fe6e:14cd%eth1`
` n11| 8737AC1| 23XXH12|31d90128-5c37-11e1-bdb7-5cf3fc6e4920|5c:f3:fc:6e:15:31| lenovo-imm2| 172.30.3.11,fe80::5ef3:fcff:fe6e:1531%eth1`
` n13| 8737AC1| 23XXH44|e23a138a-5cd3-11e1-8f3d-5cf3fc6e4950|5c:f3:fc:6e:15:49| lenovo-imm2| 172.30.3.13,fe80::5ef3:fcff:fe6e:1549%eth1`
` | 8737AC1| 23XXH29|5cd1216b-5c37-11e1-ba0c-5cf3fc6e49c8|5c:f3:fc:6e:15:85| lenovo-imm2| 172.30.3.9,fe80::5ef3:fcff:fe6e:1585%eth1`
` | 8737AC1| 23ZYT44|f4bf48ca-71f0-11e1-b274-5cf3fc6e4f10|5c:f3:fc:6e:18:29| lenovo-imm2| 172.30.3.10,fe80::5ef3:fcff:fe6e:1829%eth1`
` hpcedr| 7915AC1| 06DRHL5|a64e3014-d7e3-11e1-8d21-6cae8b1dff32|6c:ae:8b:1d:ff:36| lenovo-imm2| 172.30.254.250,fe80::6eae:8bff:fe1d:ff36%eth1`
` | 8737AC1| 06YRWC3|3af85a51-7efd-11e3-8599-000af7482e00|6c:ae:8b:32:cb:c5| lenovo-imm2| 172.30.3.6,fe80::6eae:8bff:fe32:cbc5%eth1`
` | 8737AC1| 06YRWB7|b230f62e-7efd-11e3-9773-000af7482980|6c:ae:8b:32:cd:01| lenovo-imm2| 172.30.3.5,fe80::6eae:8bff:fe32:cd01%eth1`
` | 8737AC1| 06YRWC7|09586005-7efe-11e3-9f03-000af7482df0|6c:ae:8b:32:cd:a5| lenovo-imm2| 172.30.3.2,fe80::6eae:8bff:fe32:cda5%eth1`
* Manually assign a single node according to serial number:
`[root@odin ~]# nodediscover assign -s 06PBX15 -n n12`
`Assigned: n12`
* Bulk execute discovery based on spreadsheet:
`[root@odin ~]# nodediscover assign -i import.csv`
`Defined r2`
`Discovered r2`
`Defined c1`
`Discovered c1`
+14 -1
View File
@@ -4,10 +4,23 @@ nodefirmware(8) -- Report firmware information on confluent nodes
## SYNOPSIS
`nodefirmware <noderange>`
`nodefirmware <noderange> <components>|core`
`nodefirmware <noderange> update [--backup] <filename>`
## DESCRIPTION
`nodefirmware` reports various pieces of firmware on confluent nodes.
`nodefirmware` reports and updates various firmware on nodes. By default it
will retrieve all firmware, but can be directed to fetch specific firmware by
calling out the name of the firmware (e.g. uefi or xcc) or request reading only
core firmware firmware by using the word 'core', which is generally a quicker
operation.
In the update form, it accepts a single file and attempts to update it using
the out of band facilities. Firmware updates can end in one of three states:
* `error`: The attempted update encountered an error that prevented successful install. Nodes experiencing this state will be reported at the end and more detail on the error will be given
* `pending`: The firmware update process has completed, but the firmware will not be active until the relevant component next resets. Generally speaking, for UEFI update the system will need a reboot, and for BMC updates, the `nodebmcreset` command will begin the process to activate the firmware.
* `complete`: The firmware update process has completed and activation has proceeded. Note that while the activation process has commenced, the component may still be in the process of rebooting when nodefirmware exits.
## EXAMPLES
@@ -0,0 +1,23 @@
nodegroupdefine(8) -- Define new confluent node group
## SYNOPSIS
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodegroupdefine` allows the definition of a new node 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.
## EXAMPLES
* Create a group called `compute`:
`# nodegroupdefine compute`
`compute: created`
## SEE ALSO
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
@@ -0,0 +1,18 @@
nodegroupremove(8) -- Remove a nodegroup from the confluent database
====================================================================
## SYNOPSIS
`nodegroupremove <noderange>`
## DESCRIPTION
`nodegroupremove` simply removes the given single nodegroup from the confluent database.
## EXAMPLES
* Remove group called testgroup
`# nodegroupremove testgroup`
`testgroup: deleted`
+17 -2
View File
@@ -3,13 +3,15 @@ nodeinventory(8) -- Get hardware inventory of confluent node
## SYNOPSIS
`nodeinventory <noderange>`
`nodeinventory <noderange> [serial|model|uuid|mac]`
## DESCRIPTION
`nodeinventory` pulls information about hardware of a node. This includes
information such as adapters, serial numbers, processors, and memory modules,
as supported by the platforms hardware management implementation.
as supported by the platforms hardware management implementation. It accepts
arguments such as serial or model or others as listed above to filter
output to specific data.
## EXAMPLES
@@ -104,3 +106,16 @@ as supported by the platforms hardware management implementation.
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 4: 40:f2:e9:af:45:a3`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller PCI slot: 1b:00`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller location: Onboard`
* Reporting just the mac addresses of a node named r2:
`# nodeinventory r2 mac`
`r2: System MAC Address 1: 40:f2:e9:b9:10:18`
`r2: System MAC Address 2: 40:f2:e9:b9:10:19`
`r2: System MAC Address 3: 40:f2:e9:b9:10:1a`
`r2: System MAC Address 4: 40:f2:e9:b9:10:1b`
* Getting the model number and serial number of a node named s2:
`# nodeinventory stark2 serial model`
`s2: System Product name: LENOVO THINKSYSTEM SD530 SERVER`
`s2: System Serial Number: DEV0000002`
`s2: System Model: 7X2104Z028`
+57
View File
@@ -0,0 +1,57 @@
nodemedia(8) -- Manage server remote media
=========================================================
## SYNOPSIS
`nodemedia <noderange> [attach|detachall|list|upload] [options] <media>`
## DESCRIPTION
**nodemedia** manages the remote media functionality of supported BMCs.
`list` shows all the current remote media the BMCs of the noderange are
providing to the host platform. The string (insecure) is appended to URLs that
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. 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.
`upload` takes the given media image and uploads it to the BMC. This causes
the remote media to reside internally to the system without having to go
to the network after the upload. This is more constrained, for example the
Lenovo xClarity Controller has a limit of 50 megabytes, but it has zero ongoing
load on the media source.
`attach` takes a URL to a remote media as an argument, and has the given
BMCs map a virtual USB device to that url. Content is loaded on demand, and
as such that URL is referenced potentially once for every IO operation that
the host platform attempts.
## OPTIONS
* `-c`:
Specify the maximum number of instances to run concurrently.
## EXAMPLES
* Listing currently mounted media:
`# nodemedia s1-s4 list`
`s1: boot.img`
`s2: boot.img`
`s4: boot.img`
* Uploading a small boot image to the BMC:
`# nodemedia s1-s4 upload boot.img`
`s1:complete: 100% s2:complete: 100% s3:complete: 100% s4:complete: 100%`
`s1: boot.img`
`s4: boot.img`
`s2: boot.img`
* Attaching a larger ISO for on-demand access:
`# nodemedia s1,s4 attach http://172.30.0.6/install/rhel74.iso`
`s4: http://172.30.0.6/install/rhel74.iso (insecure)`
`s1: http://172.30.0.6/install/rhel74.iso (insecure)`
+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`
+57
View File
@@ -0,0 +1,57 @@
noderange(5) -- Indicate a target set of nodes in confluent
=========================================================
## SYNOPSIS
`<noderange>`
## DESCRIPTION
<noderange> is a syntax that can be used in most Confluent commands to conveniently specify a list of nodes. The result is that the command will be applied to a range of nodes, often in parallel.
## EXAMPLES:
The simplest noderange is a single node name:
`n1`
Nodes and groups may be used interchangeably. The following may be used in any context where n1 would be accepted as a noderange:
`rack1`
Commonly, there is a desire to target a range of elements. There are a few identically behaving syntaxes for the purpose.
`n1:n20`
`n1-n20`
`n[1-20]`
Note that numbers may be zero padded or not, it will automatically detect the padding amount and adjust members of the range accordingly. Ranges also can understand multiple numeric values changing:
`r[1-3]u[01-10]`
`r1u01-r3u10`
Ranges can also be applied to group names, and all above syntaxes are compatible:
`rack1-rack10`
`rack[1-10]`
Also, regular expressions may be used to indicate nodes with names matching certain patterns:
`~r1u..`
The other major noderange primitive is indicating nodes by some attribute value:
`location.rack=7`
Commas can be used to indicate multiple nodes, and can mix and match any of the above primitives. The following can be a valid single noderange, combining any and all members of each comma separated component
`n1,n2,rack1,storage,location.rack=9,~s1..,n20-n30`
Exclusions can be done by prepending a ‘-‘ before a portion of a noderange:
`rack1,-n2`
`compute,-rack1`
`compute,-location.row=12`
To indicate nodes that match multiple selections at once (set intersection), the @ symbol may be used:
`compute@rack1`
`location.rack=10@compute`
For complex expressions, () may be used to indicate order of expanding the noderange to be explicit
`rack1,-(console.logging=full@compute)`
Noderange syntax can also indicate ‘pagination’, or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
`rack1>3<6`
The above would show the seventh through ninth nodes of the rack1 group. Like all other noderange operations, this may be combined with any of the above, but must appear as the very last operation. Ordering is done with a natural sort.
+25
View File
@@ -0,0 +1,25 @@
noderemove(8) -- Remove nodes from the confluent management service
===================================================================
## SYNOPSIS
`noderemove <noderange>`
## DESCRIPTION
`noderemove` simply removes the given noderange from the confluent database.
## EXAMPLES
* Remove two racks each with 4 nodes:
`# noderemove r1u1-r2u4`
`r1u4: deleted`
`r1u1: deleted`
`r1u2: deleted`
`r1u3: deleted`
`r2u4: deleted`
`r2u3: deleted`
`r2u2: deleted`
`r2u1: deleted`
+19
View File
@@ -0,0 +1,19 @@
nodereseat(8) -- Request a reseat of a node
============================================
## SYNOPSIS
`nodereseat <noderange>`
## DESCRIPTION
`nodereseat` requests the enclosure manager of the current node to reseat that
node's slot. This should be equivalent to removing the system entirely from
the chassis and putting it back in, but without actually having to do so.
## EXAMPLES
* Reseating the node `s1`:
`# nodereseat s1`
`s1: Reseat successful`
+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)
+1 -1
View File
@@ -4,7 +4,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
## SYNOPSIS
`nodesetboot <noderange>`
`nodesetboot [options] <noderang> [default|cd|network|setup|hd]`
`nodesetboot [options] <noderange> [default|cd|network|setup|hd]`
## DESCRIPTION
+12 -3
View File
@@ -3,13 +3,19 @@ nodeshell(8) -- Execute command on many nodes in a noderange through ssh
## SYNOPSIS
`nodeshell <noderange> <command to execute on each node>`
`nodeshell <noderange> [options] <command to execute on each node>`
## DESCRIPTION
Allows execution of a command on many nodes in parallel. Like noderun(8), it
accepts and interpolates confluent attribute expressions as documented in
nodeattribexpressions(5).
nodeattribexpressions(5). `nodeshell` provides stdout as stdout and stderr
as stderr, unlike psh which combines all stdout and stderr into stdout.
## OPTIONS
* `-c`:
Specify the maximum number of instances to run concurrently.
## EXAMPLES
@@ -21,7 +27,10 @@ nodeattribexpressions(5).
`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
+2
View File
@@ -1,8 +1,10 @@
#!/bin/sh
cd `dirname $0`/doc/man
mkdir -p ../../man/man1
mkdir -p ../../man/man5
mkdir -p ../../man/man8
ronn -r *.ronn
mv *.1 ../../man/man1/
mv *.5 ../../man/man5/
mv *.8 ../../man/man8/
@@ -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))
+1
View File
@@ -4,6 +4,7 @@ import os
data_files = [('/etc/profile.d', ['confluent_env.sh', 'confluent_env.csh'])]
try:
scriptlist = ['bin/{0}'.format(d) for d in os.listdir('bin/')]
data_files.append(('/opt/confluent/share/man/man1', ['man/man1/' + x for x in os.listdir('man/man1')]))
data_files.append(('/opt/confluent/share/man/man5', ['man/man5/' + x for x in os.listdir('man/man5')]))
data_files.append(('/opt/confluent/share/man/man8', ['man/man8/' + x for x in os.listdir('man/man8')]))
except OSError:
+1
View File
@@ -1,2 +1,3 @@
include sysvinit/*
include systemd/*
include sysctl/*
+121
View File
@@ -0,0 +1,121 @@
#!/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)
print(res.get('collective',
{'status': 'Unknown response: ' + repr(res)})['status'])
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 -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,494 @@
# 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.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 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
cfginitlock = None
follower = None
retrythread = None
class ContextBool(object):
def __init__(self):
self.active = False
def __enter__(self):
self.active = True
def __exit__(self, exc_type, exc_val, exc_tb):
self.active = False
connecting = ContextBool()
leader_init = ContextBool()
def connect_to_leader(cert=None, name=None, leader=None):
global currentleader
global cfginitlock
global follower
if cfginitlock is None:
cfginitlock = threading.RLock()
if leader is None:
leader = currentleader
try:
remote = connect_to_collective(cert, leader)
except socket.error:
return False
with connecting:
with cfginitlock:
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:
eventlet.spawn_after(random.random(), connect_to_leader,
cert, name, leader)
return True
if 'leader' in keydata:
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:
return become_leader(remote)
print(keydata['error'])
return False
if follower is not None:
follower.kill()
cfm.stop_following()
follower = None
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
cfm.follow_channel(remote)
# 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()
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 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
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 myself != get_leader(connection):
tlvdata.send(
connection,
{'error': 'Cannot assimilate, our leader is '
'in another castle', 'leader': currentleader})
connection.close()
return
if connecting.active:
tlvdata.send(connection, {'error': 'Connecting right now',
'backoff': True})
connection.close()
return
if request['txcount'] > cfm._txcount:
retire_as_leader()
tlvdata.send(connection,
{'error': 'Client has higher tranasaction count, '
'should assimilate me, connecting..',
'txcount': cfm._txcount})
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 answer and 'error' in answer:
connect_to_leader(None, None, leader=remote.getpeername()[0])
def get_leader(connection):
if currentleader is None or connection.getpeername()[0] == currentleader:
become_leader(connection)
return currentleader
def retire_as_leader():
global currentleader
cfm.stop_leading()
currentleader = None
def become_leader(connection):
global currentleader
global follower
if follower:
follower.kill()
follower = None
currentleader = connection.getsockname()[0]
skipaddr = connection.getpeername()[0]
myname = get_myname()
for member in cfm.list_collective():
dronecandidate = cfm.get_collective_member(member)['address']
if dronecandidate in (currentleader, skipaddr) or member == myname:
continue
eventlet.spawn_n(try_assimilate, dronecandidate)
def startup():
global cfginitlock
members = list(cfm.list_collective())
if len(members) < 2:
# Not in collective mode, return
return
if cfginitlock is None:
cfginitlock = threading.RLock()
eventlet.spawn_n(start_collective)
def start_collective():
global follower
global retrythread
if follower:
follower.kill()
follower = None
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']
if connect_to_leader(name=myname, leader=ldrcandidate):
break
else:
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
@@ -148,6 +148,14 @@ 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.policy': {
'description': 'Policy to use for auto-configuration of discovered '
'and identified nodes. Valid values are "manual", '
@@ -279,12 +287,17 @@ node = {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
},
'enclosure.manager': {
'description': "The management device for this node's chassis",
# 'appliesto': ['system'],
},
'enclosure.bay': {
'description': 'The bay in the enclosure, if any',
# 'appliesto': ['system'],
},
'enclosure.extends': {
'description': 'When using an extendable enclosure, this is the node '
'representing the manager that is one closer to the '
'uplink.',
},
'enclosure.manager': {
'description': "The management device for this node's chassis",
# 'appliesto': ['system'],
},
@@ -293,17 +306,39 @@ node = {
#IBM Flex)''',
# 'appliesto': ['system'],
# },
# 'id.serial': {
# 'description': 'The manufacturer serial number of node',
# },
'id.model': {
'description': 'The model number of a node. In scenarios where there '
'is both a name and a model number, it is generally '
'expected that this would be the generally more '
'specific model number.'
},
'id.serial': {
'description': 'The manufacturer serial number of node',
},
'id.uuid': {
'description': 'The UUID of the node as presented in DMI.',
},
# For single interface mac collection, net.bootable suffices
# For multiple interface mac collection, we perhaps add an address field and use subnet affinity as a clue
# to disambiguate multiple addresses for external provisioning
# For internal provisioning, the UUID matters rather than the MAC, though subnet affinity may
# still be a factor to select the appropriate static config to send down. In such a case bonding
# is hopefully more likely as that's a bit easier.
# Start with the first case and only document that, the other thoughts can be future items if they turn up
'net.bootable': {
'type': bool,
'description': 'Whether or not the indicated network interface is to be used for booting. This is used by '
'the discovery process to decide where to place the mac address of a detected PXE nic.',
},
'net.ipv4_gateway': {
'description': 'The IPv4 gateway to use if applicable. As is the '
'case for other net attributes, net.eth0.ipv4_gateway '
'and similar is accepted.'
},
'net.hwaddr': {
'description': 'The hardware address, aka MAC address of the interface indicated, generally populated by the '
'PXE discovery mechanism'
},
# 'net.pxe': { 'description': 'Whether pxe will be used on this interface'
# TODO(jjohnson2): Above being 'true' will control whether mac addresses
# are stored in this nics attribute on pxe-client discovery, since
File diff suppressed because it is too large Load Diff
+193 -19
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,13 +142,16 @@ 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
self.cfgmgr = configmanager
self.node = node
self.connectstate = 'unconnected'
@@ -190,6 +197,16 @@ class ConsoleHandler(object):
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
def _get_retry_time(self):
clustsize = len(self.cfgmgr._cfgstore['nodes'])
self._retrytime = self._retrytime * 2 + 1
if self._retrytime > 120:
self._retrytime = 120
retrytime = clustsize * 0.05 * self._retrytime
if retrytime > 120:
retrytime = 120
return retrytime + (retrytime * random.random())
def feedbuffer(self, data):
try:
self.termstream.feed(data)
@@ -203,16 +220,34 @@ 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]:
self._isondemand = False
elif (attrvalue[self.node]['console.logging']['value'] not in (
'full', '')):
self._isondemand = True
elif (attrvalue[self.node]['console.logging']['value']) == 'none':
self._dologging = False
else:
if (attrvalue[self.node]['console.logging']['value'] not in (
'full', '', 'buffer')):
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
@@ -224,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
@@ -247,7 +286,7 @@ class ConsoleHandler(object):
return
else:
self._ondemand()
if logvalue == 'none':
if logvalue in ('none', 'memory'):
self._dologging = False
if not self._isondemand or self.livesessions:
eventlet.spawn(self._connect)
@@ -272,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()
@@ -293,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
@@ -308,10 +353,10 @@ 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)
except exc.NotImplementedException:
"create", self.cfgmgr))[0]
except (exc.NotImplementedException, exc.NotFoundException):
self._console = None
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
@@ -342,7 +387,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 120 + (120 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -352,7 +397,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 120 + (120 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -364,7 +409,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 120 + (120 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -372,13 +417,13 @@ class ConsoleHandler(object):
def _got_connected(self):
self.connectstate = 'connected'
self._retrytime = 0
self.log(
logdata='console connected', ltype=log.DataTypes.event,
event=log.Events.consoleconnect)
self._send_rcpts({'connectstate': self.connectstate})
def _got_disconnected(self):
self.clearbuffer()
if self.connectstate != 'unconnected':
self.connectstate = 'unconnected'
self.log(
@@ -387,6 +432,8 @@ class ConsoleHandler(object):
self._send_rcpts({'connectstate': self.connectstate})
if self._isalive:
self._connect()
else:
self.clearbuffer()
def close(self):
self._isalive = False
@@ -399,6 +446,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
@@ -468,9 +518,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
@@ -479,6 +526,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):
@@ -554,7 +605,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:
@@ -570,11 +626,117 @@ def start_console_sessions():
def connect_node(node, configmanager, username=None):
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():
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,
@@ -666,6 +828,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
+293 -23
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',
@@ -172,7 +195,13 @@ def _init_core():
'default': 'ipmi',
}),
},
}
},
'system': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'_console': {
'session': PluginRoute({
@@ -185,6 +214,11 @@ def _init_core():
'handler': 'ssh',
}),
},
'_enclosure': {
'reseat_bay': PluginRoute(
{'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi'}),
},
'shell': {
# another special case similar to console
'sessions': PluginCollection({
@@ -211,6 +245,10 @@ def _init_core():
}),
},
},
#'forward': {
# # Another dummy value, currently only for the gui
# 'web': None,
#},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
@@ -233,13 +271,39 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'updates': {
'active': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
},
'media': {
'uploads': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'attach': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'detach': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'current': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'power': {
'state': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'reseat': PluginRoute({'handler': 'enclosure'}),
},
'sensors': {
'hardware': {
@@ -247,6 +311,10 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'energy': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -326,10 +394,12 @@ def iterate_collections(iterable, forcecollection=True):
def iterate_resources(fancydict):
for resource in fancydict.iterkeys():
for resource in fancydict:
if resource.startswith("_"):
continue
if not isinstance(fancydict[resource], PluginRoute): # a resource
if resource == 'abbreviate':
pass
elif not isinstance(fancydict[resource], PluginRoute): # a resource
resource += '/'
yield msg.ChildCollection(resource)
@@ -363,7 +433,8 @@ def delete_node_collection(collectionpath, configmanager, isnoderange):
def enumerate_nodegroup_collection(collectionpath, configmanager):
nodegroup = collectionpath[1]
if not configmanager.is_nodegroup(nodegroup):
raise exc.NotFoundException("Invalid element requested")
raise exc.NotFoundException(
'Invalid nodegroup: {0} not found'.format(nodegroup))
del collectionpath[0:2]
collection = nested_lookup(nodegroupresources, collectionpath)
return iterate_resources(collection)
@@ -383,6 +454,7 @@ def enumerate_node_collection(collectionpath, configmanager):
collection = nested_lookup(noderesources, collectionpath[2:])
if len(collectionpath) == 2 and collectionpath[0] == 'noderange':
collection['nodes'] = {}
collection['abbreviate'] = {}
if not isinstance(collection, dict):
raise exc.NotFoundException("Invalid element requested")
return iterate_resources(collection)
@@ -495,6 +567,73 @@ class BadPlugin(object):
self.node, self.plugin + ' is not a supported plugin')
def abbreviate_noderange(configmanager, inputdata, operation):
if operation != 'create':
raise exc.InvalidArgumentException('Must be a create with nodes in list')
if 'nodes' not in inputdata:
raise exc.InvalidArgumentException('Must be given list of nodes under key named nodes')
if isinstance(inputdata['nodes'], str) or isinstance(inputdata['nodes'], unicode):
inputdata['nodes'] = inputdata['nodes'].split(',')
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
@@ -507,6 +646,8 @@ def handle_node_request(configmanager, inputdata, operation,
# facility anyway
isnoderange = False
pathcomponents = pathcomponents[2:]
elif len(pathcomponents) == 3 and pathcomponents[2] == 'abbreviate':
return abbreviate_noderange(configmanager, inputdata, operation)
else:
isnoderange = True
else:
@@ -515,7 +656,8 @@ def handle_node_request(configmanager, inputdata, operation,
nodeorrange = pathcomponents[1]
if not isnoderange and not configmanager.is_node(nodeorrange):
raise exc.NotFoundException("Invalid Node")
if isnoderange:
if isnoderange and not (len(pathcomponents) == 3 and
pathcomponents[2] == 'abbreviate'):
try:
nodes = noderange.NodeRange(nodeorrange, configmanager).nodes
except Exception as e:
@@ -571,10 +713,11 @@ 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)
pathcomponents, operation, inputdata, nodes, isnoderange,
configmanager)
if 'handler' in plugroute: # fixed handler definition, easy enough
if isinstance(plugroute['handler'], str):
hfunc = getattr(pluginmap[plugroute['handler']], operation)
@@ -587,20 +730,35 @@ 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)
if plugpath is not None:
try:
hfunc = getattr(pluginmap[plugpath], operation)
@@ -611,19 +769,30 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
if badcollnodes:
raise exc.ConfluentException(
'collective management active, '
'collective.manager must be set for {0}'.format(
','.join(badcollnodes)))
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()
@@ -633,6 +802,105 @@ 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 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)
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:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} is unreachable'.format(a['name']))
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
@@ -681,7 +949,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
except IndexError: # it's just users/
if operation == 'create':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata)
pathcomponents, operation, inputdata,
configmanager=configmanager)
create_user(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_users(),
forcecollection=False)
@@ -693,7 +962,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
return delete_user(user, configmanager)
elif operation == 'update':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata)
pathcomponents, operation, inputdata,
configmanager=configmanager)
update_user(user, inputdata.attribs, configmanager)
return show_user(user, configmanager)
elif pathcomponents[0] == 'events':
+484 -108
View File
@@ -61,6 +61,7 @@
# retry until uppercase, lowercase, digit, and symbol all present)
# - Apply defined configuration to endpoint
import base64
import confluent.config.configmanager as cfm
import confluent.discovery.protocols.pxe as pxe
#import confluent.discovery.protocols.ssdp as ssdp
@@ -75,11 +76,16 @@ import confluent.messages as msg
import confluent.networking.macmap as macmap
import confluent.noderange as noderange
import confluent.util as util
import eventlet
import traceback
webclient = eventlet.import_patched('pyghmi.util.webclient')
import eventlet
import eventlet.greenpool
import eventlet.semaphore
autosensors = set()
class nesteddict(dict):
def __missing__(self, key):
@@ -91,6 +97,7 @@ nodehandlers = {
'service:management-hardware.Lenovo:lenovo-xclarity-controller': xcc,
'service:management-hardware.IBM:integrated-management-module2': imm,
'pxe-client': pxeh,
'service:io-device.Lenovo:management-module': None,
}
servicenames = {
@@ -98,6 +105,7 @@ servicenames = {
'service:lenovo-smm': 'lenovo-smm',
'service:management-hardware.Lenovo:lenovo-xclarity-controller': 'lenovo-xcc',
'service:management-hardware.IBM:integrated-management-module2': 'lenovo-imm2',
'service:io-device.Lenovo:management-module': 'lenovo-switch',
}
servicebyname = {
@@ -105,7 +113,11 @@ servicebyname = {
'lenovo-smm': 'service:lenovo-smm',
'lenovo-xcc': 'service:management-hardware.Lenovo:lenovo-xclarity-controller',
'lenovo-imm2': 'service:management-hardware.IBM:integrated-management-module2',
'lenovo-switch': 'service:io-device.Lenovo:management-module',
}
discopool = eventlet.greenpool.GreenPool(500)
runningevals = {}
# Passive-only auto-detection protocols:
# PXE
@@ -137,17 +149,60 @@ servicebyname = {
known_info = {}
known_services = {}
known_serials = {}
known_uuids = nesteddict()
known_nodes = nesteddict()
unknown_info = {}
pending_nodes = {}
pending_by_uuid = {}
def enrich_pxe_info(info):
sn = None
mn = None
nodename = info.get('nodename', None)
uuid = info.get('uuid', '')
if not uuid_is_valid(uuid):
return info
for mac in known_uuids.get(uuid, {}):
if not sn and 'serialnumber' in known_uuids[uuid][mac]:
info['serialnumber'] = known_uuids[uuid][mac]['serialnumber']
if not mn and 'modelnumber' in known_uuids[uuid][mac]:
info['modelnumber'] = known_uuids[uuid][mac]['modelnumber']
if nodename is None and 'nodename' in known_uuids[uuid][mac]:
info['nodename'] = known_uuids[uuid][mac]['nodename']
def uuid_is_valid(uuid):
if not uuid:
return False
return uuid.lower() not in ('00000000-0000-0000-0000-000000000000',
'ffffffff-ffff-ffff-ffff-ffffffffffff',
'00112233-4455-6677-8899-aabbccddeeff',
'20202020-2020-2020-2020-202020202020')
def send_discovery_datum(info):
addresses = info.get('addresses', [])
if info['handler'] == pxeh:
enrich_pxe_info(info)
yield msg.KeyValueData({'nodename': info.get('nodename', '')})
yield msg.KeyValueData({'ipaddrs': [x[0] for x in addresses]})
yield msg.KeyValueData({'serialnumber': info.get('serialnumber', '')})
yield msg.KeyValueData({'modelnumber': info.get('modelnumber', '')})
sn = info.get('serialnumber', '')
mn = info.get('modelnumber', '')
uuid = info.get('uuid', '')
if uuid:
relatedmacs = []
for mac in known_uuids.get(uuid, {}):
if mac and mac != info.get('hwaddr', ''):
relatedmacs.append(mac)
if relatedmacs:
yield msg.KeyValueData({'relatedmacs': relatedmacs})
yield msg.KeyValueData({'serialnumber': sn})
yield msg.KeyValueData({'modelnumber': mn})
yield msg.KeyValueData({'uuid': uuid})
if 'enclosure.bay' in info:
yield msg.KeyValueData({'bay': int(info['enclosure.bay'])})
yield msg.KeyValueData({'macs': [info.get('hwaddr', '')]})
types = []
for infotype in info.get('services', []):
@@ -162,6 +217,7 @@ def _info_matches(info, criteria):
node = criteria.get('by-node', None)
serial = criteria.get('by-serial', None)
status = criteria.get('by-state', None)
uuid = criteria.get('by-uuid', None)
if model and info.get('modelnumber', None) != model:
return False
if devtype and devtype not in info.get('services', []):
@@ -172,6 +228,8 @@ def _info_matches(info, criteria):
return False
if status and info.get('discostatus', None) != status:
return False
if uuid and info.get('uuid', None) != uuid:
return False
return True
@@ -193,6 +251,14 @@ def list_matching_serials(criteria):
if _info_matches(info, criteria):
yield msg.ChildCollection(serial + '/')
def list_matching_uuids(criteria):
for uuid in sorted(list(known_uuids)):
for mac in known_uuids[uuid]:
info = known_uuids[uuid][mac]
if _info_matches(info, criteria):
yield msg.ChildCollection(uuid + '/')
break
def list_matching_states(criteria):
return [msg.ChildCollection(x) for x in ('discovered/', 'identified/',
@@ -238,18 +304,19 @@ list_info = {
'by-model': list_matching_models,
'by-mac': list_matching_macs,
'by-state': list_matching_states,
'by-uuid': list_matching_uuids,
}
multi_selectors = set([
'by-type',
'by-model',
'by-state',
'by-uuid',
])
node_selectors = set([
'by-node',
#'by-uuid',
'by-serial',
])
@@ -272,7 +339,7 @@ def _parameterize_path(pathcomponents):
if key not in validselectors:
raise exc.NotFoundException('{0} is not valid here'.format(key))
if key == 'by-type':
keyparams[key] = servicebyname.get(val, None)
keyparams[key] = servicebyname.get(val, '!!!!invalid-type')
else:
keyparams[key] = val
validselectors.discard(key)
@@ -284,7 +351,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
@@ -293,24 +381,37 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
raise exc.InvalidArgumentException()
rescan()
return (msg.KeyValueData({'rescan': 'started'}),)
elif (operation in ('update', 'create')):
elif operation in ('update', 'create'):
if 'node' not in inputdata:
raise exc.InvalidArgumentException('Missing node name in input')
_, queryparms, _, _ = _parameterize_path(pathcomponents[1:])
if 'by-mac' not in queryparms:
raise exc.InvalidArgumentException('Must target using "by-mac"')
mac = queryparms['by-mac'].replace('-', ':')
if mac not in known_info:
raise exc.NotFoundException('{0} not found'.format(mac))
mac = _get_mac_from_query(pathcomponents)
info = known_info[mac]
if info['handler'] is None:
raise exc.NotImplementedException(
'Unable to {0} to {1}'.format(operation,
'/'.join(pathcomponents)))
handler = info['handler'].NodeHandler(info, configmanager)
eval_node(configmanager, handler, info, inputdata['node'],
manual=True)
return [msg.AssignedResource(inputdata['node'])]
elif operation == 'delete':
mac = _get_mac_from_query(pathcomponents)
del known_info[mac]
return [msg.DeletedResource(mac)]
raise exc.NotImplementedException(
'Unable to {0} to {1}'.format(operation, '/'.join(pathcomponents)))
def _get_mac_from_query(pathcomponents):
_, queryparms, _, _ = _parameterize_path(pathcomponents[1:])
if 'by-mac' not in queryparms:
raise exc.InvalidArgumentException('Must target using "by-mac"')
mac = queryparms['by-mac'].replace('-', ':')
if mac not in known_info:
raise exc.NotFoundException('{0} not found'.format(mac))
return mac
def handle_read_api_request(pathcomponents):
# TODO(jjohnson2): This should be more generalized...
# odd indexes into components are 'by-'*, even indexes
@@ -319,6 +420,7 @@ def handle_read_api_request(pathcomponents):
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'])
@@ -344,6 +446,14 @@ def detected_models():
def _recheck_nodes(nodeattribs, configmanager):
if rechecklock.locked():
# if already in progress, don't run again
# it may make sense to schedule a repeat, but will try the easier and less redundant way first
return
with rechecklock:
return _recheck_nodes_backend(nodeattribs, configmanager)
def _recheck_nodes_backend(nodeattribs, configmanager):
global rechecker
_map_unique_ids(nodeattribs)
# for the nodes whose attributes have changed, consider them as potential
@@ -364,18 +474,29 @@ def _recheck_nodes(nodeattribs, configmanager):
# policy or hadn't been able to verify key
for nodename in pending_nodes:
info = pending_nodes[nodename]
handler = info['handler'].NodeHandler(info, configmanager)
eventlet.spawn_n(eval_node, configmanager, handler, info, nodename)
try:
if info['handler'] is None:
next
handler = info['handler'].NodeHandler(info, configmanager)
discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
except Exception:
traceback.print_exc()
log.log({'error': 'Unexpected error during discovery of {0}, check debug '
'logs'.format(nodename)})
def _recheck_single_unknown(configmanager, mac):
info = unknown_info.get(mac, None)
_recheck_single_unknown_info(configmanager, info)
def _recheck_single_unknown_info(configmanager, info):
global rechecker
global rechecktime
info = unknown_info.get(mac, None)
if not info:
if not info or info['handler'] is None:
return
if info['handler'] != pxeh and not info.get('addresses', None):
log.log({'info': 'Missing address information in ' + repr(info)})
#log.log({'info': 'Missing address information in ' + repr(info)})
return
handler = info['handler'].NodeHandler(info, configmanager)
if handler.https_supported and not handler.https_cert:
@@ -386,9 +507,8 @@ def _recheck_single_unknown(configmanager, mac):
''.format(
handler.devname, info['hwaddr'], handler.ipaddr
)})
# addresses data is bad, clear it, to force repair next
# opportunity
info['addresses'] = []
# addresses data is bad, delete the offending ip
info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr]
# TODO(jjohnson2): rescan due to bad peer addr data?
# not just wait around for the next announce
return
@@ -398,15 +518,15 @@ def _recheck_single_unknown(configmanager, mac):
'https, will examine later'.format(
handler.devname, info['hwaddr'], handler.ipaddr
)})
if rechecker is not None and rechecktime > util.monotonic_time() + 60:
if rechecker is not None and rechecktime > util.monotonic_time() + 300:
rechecker.cancel()
# if cancel did not result in dead, then we are in progress
if rechecker is None or rechecker.dead:
rechecktime = util.monotonic_time() + 60
rechecker = eventlet.spawn_after(60, _periodic_recheck,
rechecktime = util.monotonic_time() + 300
rechecker = eventlet.spawn_after(300, _periodic_recheck,
configmanager)
return
nodename = get_nodename(configmanager, handler, info)
nodename, info['maccount'] = get_nodename(configmanager, handler, info)
if nodename:
if handler.https_supported:
dp = configmanager.get_node_attributes([nodename],
@@ -418,28 +538,32 @@ def _recheck_single_unknown(configmanager, mac):
known_nodes[nodename][info['hwaddr']] = info
info['discostatus'] = 'discovered'
return # already known, no need for more
eventlet.spawn_n(eval_node, configmanager, handler, info, nodename)
discopool.spawn_n(eval_node, configmanager, handler, info, nodename)
def safe_detected(info):
eventlet.spawn_n(eval_detected, info)
if 'hwaddr' not in info or not info['hwaddr']:
return
if info['hwaddr'] in runningevals:
# Do not evaluate the same mac multiple times at once
return
runningevals[info['hwaddr']] = discopool.spawn(eval_detected, info)
def eval_detected(info):
try:
return detected(info)
detected(info)
except Exception as e:
traceback.print_exc()
del runningevals[info['hwaddr']]
def detected(info):
global rechecker
global rechecktime
if 'hwaddr' not in info:
return # For now, require hwaddr field to proceed
# later, manual and CMM discovery may act on SN and/or UUID
for service in info['services']:
if nodehandlers.get(service, None):
if service in nodehandlers:
if service not in known_services:
known_services[service] = set([])
handler = nodehandlers[service]
@@ -448,7 +572,7 @@ def detected(info):
else: # no nodehandler, ignore for now
return
try:
snum = info['attributes']['enclosure-serial-number'][0].rstrip()
snum = info['attributes']['enclosure-serial-number'][0].strip()
if snum:
info['serialnumber'] = snum
known_serials[info['serialnumber']] = info
@@ -487,34 +611,39 @@ def detected(info):
return
known_info[info['hwaddr']] = info
cfg = cfm.ConfigManager(None)
handler = handler.NodeHandler(info, cfg)
if handler.https_supported and not handler.https_cert:
if handler:
handler = handler.NodeHandler(info, cfg)
handler.scan()
uuid = info.get('uuid', None)
if uuid_is_valid(uuid):
known_uuids[uuid][info['hwaddr']] = info
if handler and handler.https_supported and not handler.https_cert:
if handler.cert_fail_reason == 'unreachable':
log.log(
{
'info': '{0} with hwaddr {1} is not reachable at {2}'
''.format(
'info': '{0} with hwaddr {1} is not reachable by https '
'at address {2}'.format(
handler.devname, info['hwaddr'], handler.ipaddr
)})
info['addresses'] = []
info['addresses'] = [x for x in info.get('addresses', []) if x != handler.ipaddr]
return
log.log(
{'info': '{0} with hwaddr {1} at address {2} is not yet running '
'https, will examine later'.format(
handler.devname, info['hwaddr'], handler.ipaddr
)})
if rechecker is not None and rechecktime > util.monotonic_time() + 60:
if rechecker is not None and rechecktime > util.monotonic_time() + 300:
rechecker.cancel()
if rechecker is None or rechecker.dead:
rechecktime = util.monotonic_time() + 60
rechecker = eventlet.spawn_after(60, _periodic_recheck, cfg)
rechecktime = util.monotonic_time() + 300
rechecker = eventlet.spawn_after(300, _periodic_recheck, cfg)
unknown_info[info['hwaddr']] = info
info['discostatus'] = 'unidentfied'
#TODO, eventlet spawn after to recheck sooner, or somehow else
# influence periodic recheck to shorten delay?
return
nodename = get_nodename(cfg, handler, info)
if nodename and handler.https_supported:
nodename, info['maccount'] = get_nodename(cfg, handler, info)
if nodename and handler and handler.https_supported:
dp = cfg.get_node_attributes([nodename],
('pubkeys.tls_hardwaremanager',))
lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
@@ -527,9 +656,9 @@ def detected(info):
#TODO(jjohnson2): We might have to get UUID for certain searches...
#for now defer probe until inside eval_node. We might not have
#a nodename without probe in the future.
if nodename:
if nodename and handler:
eval_node(cfg, handler, info, nodename)
else:
elif handler:
log.log(
{'info': 'Detected unknown {0} with hwaddr {1} at '
'address {2}'.format(
@@ -539,31 +668,187 @@ def detected(info):
unknown_info[info['hwaddr']] = info
def b64tohex(b64str):
bd = base64.b64decode(b64str)
return ''.join(['{0:02x}'.format(ord(x)) for x in bd])
def get_enclosure_chain_head(nodename, cfg):
ne = True
members = [nodename]
while ne:
ne = cfg.get_node_attributes(
nodename, 'enclosure.extends').get(nodename, {}).get(
'enclosure.extends', {}).get('value', None)
if not ne:
return nodename
if ne in members:
raise exc.InvalidArgumentException(
'Circular chain that includes ' + nodename)
nodename = ne
members.append(nodename)
return nodename
def get_chained_smm_name(nodename, cfg, handler, nl=None, checkswitch=True):
# nodename is the head of the chain, cfg is a configmanager, handler
# is the handler of the current candidate, nl is optional indication
# of the next link in the chain, checkswitch can disable the switch
# search if not indicated by current situation
# returns the new name and whether it has been securely validated or not
# first we check to see if directly connected
mycert = handler.https_cert
if checkswitch:
fprints = macmap.get_node_fingerprints(nodename, cfg)
for fprint in fprints:
if util.cert_matches(fprint[0], mycert):
# ok we have a direct match, it is this node
return nodename, fprint[1]
# ok, unable to get it, need to traverse the chain from the beginning
if not nl:
nl = list(cfg.filter_node_attributes(
'enclosure.extends=' + nodename))
while nl:
if len(nl) != 1:
raise exc.InvalidArgumentException('Multiple enclosures trying to '
'extend a single enclosure')
cd = cfg.get_node_attributes(nodename, ['hardwaremanagement.manager',
'pubkeys.tls_hardwaremanager'])
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
for fprint in get_smm_neighbor_fingerprints(smmaddr, cv):
if util.cert_matches(fprint, mycert):
# a trusted chain member vouched for the cert
# so it's validated
return nl[0], True
# advance down the chain by one and try again
nodename = nl[0]
nl = list(cfg.filter_node_attributes(
'enclosure.extends=' + nodename))
return None, False
def get_smm_neighbor_fingerprints(smmaddr, cv):
if ':' in smmaddr:
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
yield 'sha256$' + b64tohex(neighs[idx]['sha256'])
def get_nodename(cfg, handler, info):
if not handler.https_supported:
curruuid = info['uuid']
nodename = nodes_by_uuid.get(curruuid, None)
if nodename is None:
# TODO: if there are too many matches on port for a
# given type, error! Can't just arbitarily limit,
# shared nic with vms is possible and valid
nodename = macmap.find_node_by_mac(info['hwaddr'], cfg)
return nodename
currcert = handler.https_cert
if not currcert:
info['discofailure'] = 'nohttps'
return None
currprint = util.get_fingerprint(currcert)
nodename = nodes_by_fprint.get(currprint, None)
nodename = None
maccount = None
info['verified'] = False
if not handler:
return None, None
if handler.https_supported:
currcert = handler.https_cert
if not currcert:
info['discofailure'] = 'nohttps'
return None, None
currprint = util.get_fingerprint(currcert, 'sha256')
nodename = nodes_by_fprint.get(currprint, None)
if not nodename:
# Try SHA512 as well
currprint = util.get_fingerprint(currcert)
nodename = nodes_by_fprint.get(currprint, None)
if not nodename:
nodename = macmap.find_node_by_mac(info['hwaddr'], cfg)
curruuid = info.get('uuid', None)
if uuid_is_valid(curruuid):
nodename = nodes_by_uuid.get(curruuid, None)
if nodename is None:
_map_unique_ids()
nodename = nodes_by_uuid.get(curruuid, None)
if not nodename:
# Ok, see if it is something with a chassis-uuid and discover by
# chassis
nodename = get_nodename_from_enclosures(cfg, info)
if not nodename and handler.devname == 'SMM':
nodename = get_nodename_from_chained_smms(cfg, handler, info)
if not nodename: # as a last resort, search switches for info
# This is the slowest potential operation, so we hope for the
# best to occur prior to this
nodename, macinfo = macmap.find_nodeinfo_by_mac(info['hwaddr'], cfg)
maccount = macinfo['maccount']
if nodename:
if handler.devname == 'SMM':
nl = list(cfg.filter_node_attributes(
'enclosure.extends=' + nodename))
if nl:
# We found an SMM, and it's in a chain per configuration
# we need to ask the switch for the fingerprint to see
# if we have a match or not
newnodename, v = get_chained_smm_name(nodename, cfg,
handler, nl)
if newnodename:
# while this started by switch, it was disambiguated
info['verified'] = v
return newnodename, None
if (nodename and
not handler.discoverable_by_switch(macinfo['maccount'])):
if handler.devname == 'SMM':
errorstr = 'Attempt to discover SMM by switch, but chained ' \
'topology or incorrect net attributes detected, ' \
'which is not compatible with switch discovery ' \
'of SMM, nodename would have been ' \
'{0}'.format(nodename)
log.log({'error': errorstr})
return None, None
return nodename, maccount
def get_nodename_from_chained_smms(cfg, handler, info):
nodename = None
for fprint in get_smm_neighbor_fingerprints(
handler.ipaddr, lambda x: True):
if fprint in nodes_by_fprint:
# need to chase the whole chain
# to support either direction
chead = get_enclosure_chain_head(nodes_by_fprint[fprint],
cfg)
newnodename, v = get_chained_smm_name(
chead, cfg, handler, checkswitch=False)
if newnodename:
info['verified'] = v
nodename = newnodename
return nodename
def get_nodename_from_enclosures(cfg, info):
nodename = None
cuuid = info.get('attributes', {}).get('chassis-uuid', [None])[0]
if cuuid and cuuid in nodes_by_uuid:
encl = nodes_by_uuid[cuuid]
bay = info.get('enclosure.bay', None)
if bay:
tnl = cfg.filter_node_attributes('enclosure.manager=' + encl)
tnl = list(
cfg.filter_node_attributes('enclosure.bay={0}'.format(bay),
tnl))
if len(tnl) == 1:
# This is not a secure assurance, because it's by
# uuid instead of a key
nodename = tnl[0]
return nodename
def eval_node(cfg, handler, info, nodename, manual=False):
try:
handler.probe() # unicast interrogation as possible to get more data
# for now, we search switch only, ideally we search cmm, smm, and
# switch concurrently
# do some preconfig, for example, to bring a SMM online if applicable
handler.preconfig()
@@ -594,6 +879,7 @@ def eval_node(cfg, handler, info, nodename, manual=False):
if not handler.is_enclosure and nl:
# The specified node is an enclosure (has nodes mapped to it), but
# what we are talking to is *not* an enclosure
# might be ambiguous, need to match chassis-uuid as well..
if 'enclosure.bay' not in info:
unknown_info[info['hwaddr']] = info
info['discostatus'] = 'unidentified'
@@ -605,11 +891,39 @@ def eval_node(cfg, handler, info, nodename, manual=False):
raise exc.InvalidArgumentException(errorstr)
log.log({'error': errorstr})
return
enl = list(cfg.filter_node_attributes('enclosure.extends=' + nodename))
if enl:
# ambiguous SMM situation according to the configuration, we need
# to match uuid
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*'
# if manual:
# raise exc.InvalidArgumentException(errorstr)
# log.log({'error': errorstr})
if encuuid in pending_by_uuid:
pending_by_uuid[encuuid].add(info)
else:
pending_by_uuid[encuuid] = set([info])
return
# We found the real smm, replace the list with the actual smm
# to continue
nl = list(cfg.filter_node_attributes(
'enclosure.manager=' + enl[0]))
else:
errorstr = 'Chained SMM configuration with older XCC, ' \
'unable to perform zero power discovery'
if manual:
raise exc.InvalidArgumentException(errorstr)
log.log({'error': errorstr})
return
# search for nodes fitting our description using filters
# lead with the most specific to have a small second pass
nl = cfg.filter_node_attributes(
'enclosure.bay=' + info['enclosure.bay'], nl)
nl = list(nl)
nl = list(cfg.filter_node_attributes(
'enclosure.bay={0}'.format(info['enclosure.bay']), nl))
if len(nl) != 1:
info['discofailure'] = 'ambigconfig'
if len(nl):
@@ -636,6 +950,28 @@ def eval_node(cfg, handler, info, nodename, manual=False):
pending_nodes[nodename] = info
else:
# we can and did accurately discover by switch or in enclosure
# 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 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 not discover_node(cfg, handler, info, nodename, manual):
pending_nodes[nodename] = info
@@ -650,14 +986,20 @@ def discover_node(cfg, handler, info, nodename, manual):
'pubkeys.tls_hardwaremanager'))
policy = dp.get(nodename, {}).get('discovery.policy', {}).get(
'value', None)
if policy is None:
policy = ''
policies = set(policy.split(','))
lastfp = dp.get(nodename, {}).get('pubkeys.tls_hardwaremanager',
{}).get('value', None)
# TODO(jjohnson2): permissive requires we guarantee storage of
# the pubkeys, which is deferred for a little bit
# Also, 'secure', when we have the needed infrastructure done
# in some product or another.
if (policy == 'permissive' and handler.https_supported and lastfp and
not manual):
curruuid = info.get('uuid', False)
if 'pxe' in policies and info['handler'] == pxeh:
return do_pxe_discovery(cfg, handler, info, manual, nodename, policies)
elif ('permissive' in policies and handler.https_supported and lastfp and
not util.cert_matches(lastfp, handler.https_cert) and not manual):
info['discofailure'] = 'fingerprint'
log.log({'info': 'Detected replacement of {0} with existing '
'fingerprint and permissive discovery policy, not '
@@ -665,33 +1007,18 @@ def discover_node(cfg, handler, info, nodename, manual):
'pubkeys.tls_hardwaremanager attribute is cleared '
'first'.format(nodename)})
return False # With a permissive policy, do not discover new
elif policy in ('open', 'permissive') or manual:
elif policies & set(('open', 'permissive')) or manual:
info['nodename'] = nodename
if not handler.https_supported:
# use uuid based scheme in lieu of tls cert, ideally only
# for stateless 'discovery' targets like pxe, where data does not
# change
if info['uuid'] in known_pxe_uuids:
return True
uuidinfo = cfg.get_node_attributes(nodename, 'id.uuid')
known_pxe_uuids[info['uuid']] = nodename
# TODO(jjohnson2): This is messing with the attrib database
# so it should only be possible if policy is 'open'
#
if manual or policy == 'open':
olduuid = uuidinfo.get(nodename, {}).get('id.uuid', None)
if 'uuid' in info and info['uuid'] != olduuid:
cfg.set_node_attributes(
{nodename: {'id.uuid': info['uuid']}})
log.log({'info': 'Detected {0} ({1} with mac {2})'.format(
nodename, handler.devname, info['hwaddr'])})
return True
if info['handler'] == pxeh:
return do_pxe_discovery(cfg, handler, info, manual, nodename, policies)
elif manual or not util.cert_matches(lastfp, handler.https_cert):
# only 'discover' if it is not the same as last time
try:
handler.config(nodename)
except Exception as e:
info['discofailure'] = 'bug'
if manual:
raise
log.log(
{'error':
'Error encountered trying to set up {0}, {1}'.format(
@@ -701,14 +1028,20 @@ def discover_node(cfg, handler, info, nodename, manual):
newnodeattribs = {}
if 'uuid' in info:
newnodeattribs['id.uuid'] = info['uuid']
if 'serialnumber' in info:
newnodeattribs['id.serial'] = info['serialnumber']
if 'modelnumber' in info:
newnodeattribs['id.model'] = info['modelnumber']
if handler.https_cert:
newnodeattribs['pubkeys.tls_hardwaremanager'] = \
util.get_fingerprint(handler.https_cert)
util.get_fingerprint(handler.https_cert, 'sha256')
if newnodeattribs:
cfg.set_node_attributes({nodename: newnodeattribs})
log.log({'info': 'Discovered {0} ({1})'.format(nodename,
handler.devname)})
info['discostatus'] = 'discovered'
for i in pending_by_uuid.get(curruuid, []):
eventlet.spawn_n(_recheck_single_unknown_info, cfg, i)
return True
log.log({'info': 'Detected {0}, but discovery.policy is not set to a '
'value allowing discovery (open or permissive)'.format(
@@ -717,6 +1050,39 @@ def discover_node(cfg, handler, info, nodename, manual):
return False
def do_pxe_discovery(cfg, handler, info, manual, nodename, policies):
# use uuid based scheme in lieu of tls cert, ideally only
# for stateless 'discovery' targets like pxe, where data does not
# change
uuidinfo = cfg.get_node_attributes(nodename, ['id.uuid', 'id.serial', 'id.model', 'net*.bootable'])
if manual or policies & set(('open', 'pxe')):
enrich_pxe_info(info)
attribs = {}
olduuid = uuidinfo.get(nodename, {}).get('id.uuid', None)
uuid = info.get('uuid', None)
if uuid and uuid != olduuid:
attribs['id.uuid'] = info['uuid']
sn = info.get('serialnumber', None)
mn = info.get('modelnumber', None)
if sn and sn != uuidinfo.get(nodename, {}).get('id.serial', None):
attribs['id.serial'] = sn
if mn and mn != uuidinfo.get(nodename, {}).get('id.model', None):
attribs['id.model'] = mn
for attrname in uuidinfo.get(nodename, {}):
if attrname.endswith('.bootable') and uuidinfo[nodename][attrname].get('value', None):
newattrname = attrname[:-8] + 'hwaddr'
attribs[newattrname] = info['hwaddr']
if attribs:
cfg.set_node_attributes({nodename: attribs})
if info['uuid'] in known_pxe_uuids:
return True
if uuid_is_valid(info['uuid']):
known_pxe_uuids[info['uuid']] = nodename
log.log({'info': 'Detected {0} ({1} with mac {2})'.format(
nodename, handler.devname, info['hwaddr'])})
return True
attribwatcher = None
nodeaddhandler = None
needaddhandled = False
@@ -737,12 +1103,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'), _recheck_nodes)
'hardwaremanagement.manager', 'net*.switchport',
'id.uuid', 'pubkeys.tls_hardwaremanager',
'net*.bootable'), _recheck_nodes)
if nodeaddhandler:
needaddhandled = True
else:
@@ -758,21 +1126,18 @@ def _periodic_recheck(configmanager):
global rechecker
global rechecktime
rechecker = None
# There shouldn't be anything causing this to double up, but just in case
# use a semaphore to absolutely guarantee this doesn't multiply
with rechecklock:
try:
_recheck_nodes((), configmanager)
except Exception:
traceback.print_exc()
log.log({'error': 'Unexpected error during discovery, check debug '
'logs'})
# if rechecker is set, it means that an accelerated schedule
# for rechecker was requested in the course of recheck_nodes
if rechecker is None:
rechecktime = util.monotonic_time() + 900
rechecker = eventlet.spawn_after(900, _periodic_recheck,
configmanager)
try:
_recheck_nodes((), configmanager)
except Exception:
traceback.print_exc()
log.log({'error': 'Unexpected error during discovery, check debug '
'logs'})
# if rechecker is set, it means that an accelerated schedule
# for rechecker was requested in the course of recheck_nodes
if rechecker is None:
rechecktime = util.monotonic_time() + 900
rechecker = eventlet.spawn_after(900, _periodic_recheck,
configmanager)
def rescan():
@@ -791,14 +1156,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 = {}
@@ -820,31 +1194,33 @@ def _map_unique_ids(nodes=None):
uuid_by_nodes = {}
fprint_by_nodes = {}
for uuid in nodes_by_uuid:
if not uuid_is_valid():
continue
node = nodes_by_uuid[uuid]
if node in bigmap:
uuid_by_nodes[node] = uuid
for fprint in nodes_by_fprint:
node = nodes_by_fprint[fprint]
if node in bigmap:
fprint_by_nodes[node] =fprint
fprint_by_nodes[node] = fprint
for node in bigmap:
if node in uuid_by_nodes:
del nodes_by_uuid[uuid_by_nodes[node]]
if node in fprint_by_nodes:
del nodes_by_fprint[fprint_by_nodes[node]]
uuid = bigmap[node].get('id.uuid', {}).get('value', None)
if uuid:
if uuid_is_valid(uuid):
nodes_by_uuid[uuid] = node
fprint = bigmap[node].get(
'pubkeys.tls_hardwaremanager', {}).get('value', None)
if fprint:
nodes_by_fprint[fprint] = node
for uuid in known_pxe_uuids:
if uuid not in nodes_by_uuid:
if uuid_is_valid(uuid) and uuid not in nodes_by_uuid:
nodes_by_uuid[uuid] = known_pxe_uuids[uuid]
if __name__ == '__main__':
start_detection()
while True:
eventlet.sleep(30)
eventlet.sleep(30)
@@ -43,6 +43,9 @@ class NodeHandler(generic.NodeHandler):
# TODO(jjohnson2): probe serial number and uuid
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(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.
@@ -88,7 +91,8 @@ class NodeHandler(generic.NodeHandler):
if ('secret.hardwaremanagementuser' not in cd or
'secret.hardwaremanagementpassword' not in cd):
raise exc.TargetEndpointBadCredentials(
'Missing user and/or password')
'secret.hardwaremanagementuser and/or '
'secret.hardwaremanagementpassword was not configured')
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
@@ -150,4 +154,4 @@ class NodeHandler(generic.NodeHandler):
raise
if reset:
ic.reset_bmc()
return
return ic
@@ -19,21 +19,31 @@ webclient = eventlet.import_patched('pyghmi.util.webclient')
class NodeHandler(object):
https_supported = True
is_enclosure = False
devname = ''
maxmacs = 2 # reasonable default, allowing for common scenario of
# shared nic in theory, but blocking enclosure managers
# and uplink ports
def __init__(self, info, configmanager):
self._certfailreason = None
self._fp = None
self.info = info
self.configmanager = configmanager
targsa = None
targsa = [None]
# first let us prefer LLA if possible, since that's most stable
for sa in info['addresses']:
if sa[0].startswith('fe80'):
targsa = sa
break
else:
targsa = info['addresses'][0]
if info.get('addresses', False):
targsa = info['addresses'][0]
self.ipaddr = targsa[0]
def scan(self):
# Do completely passive things to enhance data.
# Probe is permitted to for example attempt a login
# scan *only* does what it can without a login attempt
return
def probe(self):
@@ -44,9 +54,10 @@ class NodeHandler(object):
def preconfig(self):
return
@property
def discoverable_by_switch(self):
return True
def discoverable_by_switch(self, macs):
# Given the number of macs sharing the port, is this handler
# appropriate?
return macs <= self.maxmacs
def _savecert(self, certificate):
self._fp = certificate
@@ -15,32 +15,71 @@
import confluent.discovery.handlers.bmc as bmchandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.private.util as pygutil
import string
import struct
class NodeHandler(bmchandler.NodeHandler):
devname = 'IMM'
def probe(self):
def scan(self):
slpattrs = self.info.get('attributes', {})
self.isdense = False
try:
ff = slpattrs.get('enclosure-form-factor', [''])[0]
except IndexError:
return
wronguuid = slpattrs.get('node-uuid', [''])[0]
if wronguuid:
# we need to fix the first three portions of the uuid
uuidprefix = wronguuid.split('-')[:3]
uuidprefix = struct.pack(
'<IHH', *[int(x, 16) for x in uuidprefix]).encode('hex')
uuidprefix = uuidprefix[:8] + '-' + uuidprefix[8:12] + '-' + \
uuidprefix[12:16]
self.info['uuid'] = uuidprefix + '-' + '-'.join(
wronguuid.split('-')[3:])
self.info['uuid'] = string.lower(self.info['uuid'])
if ff not in ('dense-computing', 'BC2'):
# do not probe unless it's a dense platform
return
self.isdense = True
slot = int(slpattrs.get('slot', ['0'])[0])
if slot != 0:
self.info['enclosure.bay'] = slot
def probe(self):
if self.info.get('enclosure.bay', 0) == 0:
self.scan()
if self.info.get('enclosure.bay', 0) != 0:
# scan has already populated info
return
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff != 'dense-computing':
return
try:
# we are a dense platform, but the SLP data did not give us slot
# attempt to probe using IPMI
ipmicmd = self._get_ipmicmd()
guiddata = ipmicmd.xraw_command(netfn=6, command=8)
self.info['uuid'] = pygutil.decode_wireformat_uuid(
guiddata['data'])
guiddata['data']).lower()
ipmicmd.oem_init()
bayid = ipmicmd._oem.immhandler.get_property(
'/v2/cmm/sp/7')
if not bayid:
return
#
self.info['enclosure.bay'] = bayid
# enclosure.bay only happens for Flex, nextscale doesn't do it
# this way
self.info['enclosure.bay'] = int(bayid)
smmid = ipmicmd._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
if not smmid:
return
smmid = smmid.lower().replace(' ', '')
smmid = '{0}-{1}-{2}-{3}-{4}'.format(smmid[:8], smmid[8:12],
smmid[12:16], smmid[16:20],
smmid[20:])
self.info['enclosure.uuid'] = smmid
self.info['enclosure.type'] = 'smm'
except pygexc.IpmiException as ie:
print(repr(ie))
raise
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
# def config(self, nodename):
# return
@@ -13,10 +13,28 @@
# limitations under the License.
import confluent.discovery.handlers.bmc as bmchandler
import struct
def fixuuid(baduuid):
# SMM dumps it out in hex
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
a = struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]).encode(
'hex')
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
return '-'.join(uuid).lower()
class NodeHandler(bmchandler.NodeHandler):
is_enclosure = True
devname = 'SMM'
maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain
def scan(self):
# the UUID is in a weird order, fix it up to match
# ipmi return and property value
uuid = self.info.get('attributes', {}).get('uuid', None)
if uuid:
uuid = fixuuid(uuid[0])
self.info['uuid'] = uuid
def config(self, nodename):
# SMM for now has to reset to assure configuration applies
@@ -12,41 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.bmc as bmchandler
import confluent.discovery.handlers.imm as immhandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.private.util as pygutil
import pyghmi.ipmi.oem.lenovo.imm as imm
class NodeHandler(bmchandler.NodeHandler):
class NodeHandler(immhandler.NodeHandler):
devname = 'XCC'
def probe(self):
try:
ipmicmd = self._get_ipmicmd()
guiddata = ipmicmd.xraw_command(netfn=6, command=8)
self.info['uuid'] = pygutil.decode_wireformat_uuid(
guiddata['data'])
ipmicmd.oem_init()
bayid = ipmicmd._oem.immhandler.get_property(
'/v2/cmm/sp/7')
if not bayid:
return
self.info['enclosure.bay'] = bayid
smmid = ipmicmd._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
if not smmid:
return
smmid = smmid.lower().replace(' ', '')
smmid = '{0}-{1}-{2}-{3}-{4}'.format(smmid[:8], smmid[8:12],
smmid[12:16], smmid[16:20],
smmid[20:])
self.info['enclosure.uuid'] = smmid
self.info['enclosure.type'] = 'smm'
except pygexc.IpmiException as ie:
print(repr(ie))
raise
def preconfig(self):
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
# attempt to enable SMM
#it's normal to get a 'not supported' (193) for systems without an SMM
ipmicmd = None
@@ -54,7 +32,8 @@ class NodeHandler(bmchandler.NodeHandler):
ipmicmd = self._get_ipmicmd()
ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,))
except pygexc.IpmiException as e:
if e.ipmicode != 193:
if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and
'Incorrect password' not in str(e)):
# raise an issue if anything other than to be expected
raise
#TODO: decide how to clean out if important
@@ -62,6 +41,30 @@ class NodeHandler(bmchandler.NodeHandler):
#if ipmicmd:
# ipmicmd.ipmi_session.logout()
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)
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
# Ok, we can get the enclosure uuid now..
ic.oem_init()
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
if enclosureuuid:
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
em = self.configmanager.get_node_attributes(nodename,
'enclosure.manager')
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
'value', None)
# ok, set the uuid of the manager...
if em:
self.configmanager.set_node_attributes(
{em: {'id.uuid': enclosureuuid}})
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
# def config(self, nodename):
@@ -37,7 +37,7 @@ def decode_uuid(rawguid):
lebytes = struct.unpack_from('<IHH', buffer(rawguid[:8]))
bebytes = struct.unpack_from('>HHI', buffer(rawguid[8:]))
return '{0:08X}-{1:04X}-{2:04X}-{3:04X}-{4:04X}{5:08X}'.format(
lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2])
lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2]).lower()
def find_info_in_options(rq, optidx):
@@ -92,7 +92,7 @@ def snoop(handler):
rq = bytearray(rq)
if rq[0] == 1: # Boot request
addrlen = rq[2]
if addrlen > 16: # max address size in bootp is 16 bytes
if addrlen > 16 or addrlen == 0:
continue
netaddr = rq[28:28+addrlen]
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
@@ -16,18 +16,21 @@
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',
'service:lenovo-smm',
'service:management-hardware.Lenovo:lenovo-xclarity-controller',
'service:management-hardware.IBM:chassis-management-module',
'service:management-hardware.Lenovo:chassis-management-module',
'service:io-device.Lenovo:management-module',
])
# SLP has a lot of ambition that was unfulfilled in practice.
@@ -38,6 +41,12 @@ srvreqfooter = b'\x00\x07DEFAULT\x00\x00\x00\x00'
# which is defined in RFC 3059, used to indicate support for that capability
attrlistext = b'\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00'
try:
IPPROTO_IPV6 = socket.IPPROTO_IPV6
except AttributeError:
IPPROTO_IPV6 = 41 # Assume Windows value if socket is missing it
def _parse_slp_header(packet):
packet = bytearray(packet)
@@ -197,7 +206,7 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
v6addrs.append(('ff02::1:' + v6hash, 427, 0, 0))
for idx in util.list_interface_indexes():
# IPv6 multicast is by index, so lead with that
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
for sa in v6addrs:
try:
net.sendto(data, sa)
@@ -213,7 +222,13 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
bcast = i4['broadcast']
net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
socket.inet_aton(addr))
net4.sendto(data, ('239.255.255.253', 427))
try:
net4.sendto(data, ('239.255.255.253', 427))
except socket.error as se:
# On occasion, multicasting may be disabled
# tolerate this scenario and move on
if se.errno != 101:
raise
net4.sendto(data, (bcast, 427))
@@ -264,7 +279,7 @@ def _parse_attrlist(attrstr):
'{4:04X}{5:08X}'.format(
lebytes[0], lebytes[1], lebytes[2], bebytes[0],
bebytes[1], bebytes[2]
)
).lower()
attribs[attrname].append(val)
attrstr = attrstr[attrstr.index(')'):]
elif attrstr[0] == ',':
@@ -376,16 +391,17 @@ def snoop(handler):
:param handler:
:return:
"""
tracelog = log.Logger('trace')
active_scan(handler)
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
slpg = socket.inet_pton(socket.AF_INET6, 'ff01::123')
slpg2 = socket.inet_pton(socket.AF_INET6, 'ff02::123')
for i6idx in util.list_interface_indexes():
mreq = slpg + struct.pack('=I', i6idx)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
mreq = slpg2 + struct.pack('=I', i6idx)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -405,47 +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
peerbymacaddress[mac] = {
'services': q,
'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):
@@ -462,7 +489,7 @@ def active_scan(handler):
handler(scanned)
def scan(srvtypes=_slp_services, addresses=None):
def scan(srvtypes=_slp_services, addresses=None, localonly=False):
"""Find targets providing matching requested srvtypes
This is a generator that will iterate over respondants to the SrvType
@@ -477,13 +504,15 @@ def scan(srvtypes=_slp_services, addresses=None):
"""
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# TODO: increase RCVBUF to max, mitigate chance of
# increase RCVBUF to max, mitigate chance of
# failure due to full buffer.
net.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16777216)
# SLP is very poor at scanning large counts and managing it, so we
# must make the best of it
# Some platforms/config default to IPV6ONLY, we are doing IPv4
# too, so force it
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
#net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
# we are going to do broadcast, so allow that...
initxid = random.randint(0, 32768)
xididx = 0
@@ -502,6 +531,12 @@ def scan(srvtypes=_slp_services, addresses=None):
_grab_rsps((net, net4), rsps, 1, xidmap)
# now to analyze and flesh out the responses
for id in rsps:
if localonly:
for addr in rsps[id]['addresses']:
if 'fe80' in addr[0]:
break
else:
continue
_add_attributes(rsps[id])
del rsps[id]['payload']
del rsps[id]['function']
@@ -512,4 +547,4 @@ def scan(srvtypes=_slp_services, addresses=None):
if __name__ == '__main__':
def testsnoop(a):
print(repr(a))
snoop(testsnoop)
snoop(testsnoop)
@@ -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']:
+26 -14
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -21,79 +21,91 @@ import json
class ConfluentException(Exception):
apierrorcode = 500
apierrorstr = 'Unexpected Error'
_apierrorstr = 'Unexpected Error'
def get_error_body(self):
errstr = ' - '.join((self.apierrorstr, str(self)))
errstr = ' - '.join((self._apierrorstr, str(self)))
return json.dumps({'error': errstr })
@property
def apierrorstr(self):
if str(self):
return self._apierrorstr + ' - ' + str(self)
return self._apierrorstr
class NotFoundException(ConfluentException):
# Something that could be construed as a name was not found
# basically, picture an http error code 404
apierrorcode = 404
apierrorstr = 'Request path not recognized'
_apierrorstr = 'Target not found'
class InvalidArgumentException(ConfluentException):
# Something from the remote client wasn't correct
# like http code 400
apierrorcode = 400
apierrorstr = 'Bad Request'
_apierrorstr = 'Bad Request'
class TargetEndpointUnreachable(ConfluentException):
# A target system was unavailable. For example, a BMC
# was unreachable. http code 504
apierrorcode = 504
apierrorstr = 'Unreachable Target'
_apierrorstr = 'Unreachable Target'
class TargetEndpointBadCredentials(ConfluentException):
# target was reachable, but authentication/authorization
# failed
apierrorcode = 502
apierrorstr = 'Bad Credentials'
_apierrorstr = 'Bad Credentials'
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
apierrorstr = 'Credential store locked'
_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
apierrorstr = 'Forbidden'
_apierrorstr = 'Forbidden'
class NotImplementedException(ConfluentException):
# The current configuration/plugin is unable to perform
# the requested task. http code 501
apierrorcode = 501
apierrorstr = '501 - Not Implemented'
_apierrorstr = '501 - Not Implemented'
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
apierrorstr = 'Global configuration contains an error'
_apierrorstr = 'Global configuration contains an error'
class TargetResourceUnavailable(ConfluentException):
# This is meant for scenarios like asking to read a sensor that is
# currently unavailable. This may be a persistent or transient state
apierrocode = 503
apierrorstr = 'Target Resource Unavailable'
_apierrorstr = 'Target Resource Unavailable'
class PubkeyInvalid(ConfluentException):
apierrorcode = 502
apierrorstr = '502 - Invalid certificate or key on target'
_apierrorstr = '502 - Invalid certificate or key on target'
def __init__(self, text, certificate, fingerprint, attribname, event):
super(PubkeyInvalid, self).__init__(self, text)
self.fingerprint = fingerprint
self.attrname = attribname
bodydata = {'message': text,
'event': event,
'fingerprint': fingerprint,
@@ -106,7 +118,7 @@ class PubkeyInvalid(ConfluentException):
class LoggedOut(ConfluentException):
apierrorcode = 401
apierrorstr = '401 - Logged out'
_apierrorstr = '401 - Logged out'
def get_error_body(self):
return '{"loggedout": 1}'
@@ -0,0 +1,138 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# provide managing firmware update process and firmware repository if/when
# the time comes
import confluent.exceptions as exc
import confluent.messages as msg
import eventlet
import os
import socket
updatesbytarget = {}
uploadsbytarget = {}
updatepool = eventlet.greenpool.GreenPool(256)
def execupdate(handler, filename, updateobj, type):
if 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})
try:
if type == 'firmware':
completion = handler(filename, progress=updateobj.handle_progress,
bank=updateobj.bank)
else:
completion = handler(filename, progress=updateobj.handle_progress)
if completion is None:
completion = 'complete'
updateobj.handle_progress({'phase': completion, 'progress': 100.0})
except exc.PubkeyInvalid as pi:
errstr = 'Certificate mismatch detected, does not match value in ' \
'attribute {0}'.format(pi.attrname)
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
except Exception as e:
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': str(e)})
class Updater(object):
def __init__(self, node, handler, filename, tenant=None, name=None,
bank=None, type='firmware'):
self.bank = bank
self.node = node
self.phase = 'initializing'
self.detail = ''
self.percent = 0.0
self.updateproc = updatepool.spawn(execupdate, handler, filename,
self, type)
if type == 'firmware':
myparty = updatesbytarget
elif type == 'mediaupload':
myparty = uploadsbytarget
if (node, tenant) not in myparty:
myparty[(node, tenant)] = {}
if name is None:
name = 1
while '{0}'.format(name) in myparty[(node, tenant)]:
name += 1
self.name = '{0}'.format(name)
myparty[(node, tenant)][self.name] = self
def handle_progress(self, progress):
self.phase = progress.get('phase', 'unknown')
self.percent = float(progress.get('progress', 100.0))
self.detail = progress.get('detail', '')
def cancel(self):
self.updateproc.kill()
@property
def progress(self):
return {'phase': self.phase, 'progress': self.percent,
'detail': self.detail}
def remove_updates(nodes, tenant, element, type='firmware'):
if len(element) < 5 and element[:2] != ['media', 'uploads']:
raise exc.InvalidArgumentException()
upid = element[-1]
if type == 'firmware':
myparty = updatesbytarget
else:
myparty = uploadsbytarget
for node in nodes:
try:
upd = myparty[(node, tenant)][upid]
except KeyError:
raise exc.NotFoundException('No active update matches request')
upd.cancel()
del myparty[(node, tenant)][upid]
yield msg.DeletedResource(
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
node, upid))
def list_updates(nodes, tenant, element, type='firmware'):
showmode = False
if type == 'mediaupload':
myparty = uploadsbytarget
verb = 'upload'
else:
myparty = updatesbytarget
verb = 'update'
if type == 'firmware':
specificlen = 4
else:
specificlen = 2
if len(element) > specificlen:
showmode = True
upid = element[-1]
for node in nodes:
if showmode:
try:
updater = myparty[(node, tenant)][upid]
except KeyError:
raise exc.NotFoundException(
'No matching {0} process found'.format(verb))
yield msg.KeyValueData(updater.progress, name=node)
else:
for updateid in myparty.get((node, tenant), {}):
yield msg.ChildCollection(updateid)
+119
View File
@@ -0,0 +1,119 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#This handles port forwarding for web interfaces on management devices
#It will also hijack port 3900 and do best effort..
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
forwardersbyclient = {}
relaysbysession = {}
sessionsbyip = {}
ipsbysession = {}
sockhandler = {}
vidtargetbypeer = {}
vidforwarder = None
def handle_connection(incoming, outgoing):
while True:
r, _, _ = select.select((incoming, outgoing), (), (), 60)
for mysock in r:
data = mysock.recv(32768)
if not data:
return
if mysock == incoming:
outgoing.sendall(data)
elif mysock == outgoing:
incoming.sendall(data)
def forward_port(sock, target, clientip, sessionid):
while True:
conn, cli = sock.accept()
if cli[0] != clientip:
conn.close()
continue
try:
client = socket.create_connection((target, 443))
except Exception:
conn.close()
continue
if sessionid not in relaysbysession:
relaysbysession[sessionid] = {}
relaysbysession[sessionid][eventlet.spawn(
handle_connection, conn, client)] = conn
def forward_video():
sock = eventlet.listen(('::', 3900, 0, 0), family=socket.AF_INET6)
while True:
conn, cli = sock.accept()
if cli[0] not in vidtargetbypeer or not sessionsbyip.get(cli[0], None):
conn.close()
continue
try:
vidclient = socket.create_connection((vidtargetbypeer[cli[0]],
3900))
except Exception:
conn.close()
continue
eventlet.spawn_n(handle_connection, conn, vidclient)
def close_session(sessionid):
for addr in forwardersbyclient.get(sessionid, []):
killsock = forwardersbyclient[sessionid][addr]
sockhandler[killsock].kill()
del sockhandler[killsock]
killsock.close()
if sessionid in forwardersbyclient:
del forwardersbyclient[sessionid]
for clip in ipsbysession.get(sessionid, ()):
sessionsbyip[clip].discard(sessionid)
if sessionid in ipsbysession:
del ipsbysession[sessionid]
for relay in list(relaysbysession.get(sessionid, ())):
conn = relaysbysession[sessionid][relay]
relay.kill()
conn.close()
if sessionid in relaysbysession:
del relaysbysession[sessionid]
def get_port(addr, clientip, sessionid):
global vidforwarder
if socket.getaddrinfo(clientip, 0)[0][0] == socket.AF_INET:
clientip = '::ffff:' + clientip
if sessionid not in ipsbysession:
ipsbysession[sessionid] = set([])
if clientip not in sessionsbyip:
sessionsbyip[clientip] = set([])
sessionsbyip[clientip].add(sessionid)
ipsbysession[sessionid].add(clientip)
if sessionid not in forwardersbyclient:
forwardersbyclient[sessionid] = {}
if addr not in forwardersbyclient[sessionid]:
newsock = eventlet.listen(('::', 0, 0, 0),
family=socket.AF_INET6)
forwardersbyclient[sessionid][addr] = newsock
sockhandler[newsock] = eventlet.spawn(forward_port, newsock, addr,
clientip, sessionid)
if not vidforwarder:
vidforwarder = eventlet.spawn(forward_video)
vidtargetbypeer[clientip] = addr
return forwardersbyclient[sessionid][addr].getsockname()[1]
+35 -5
View File
@@ -21,6 +21,7 @@ import Cookie
import confluent.auth as auth
import confluent.config.attributes as attribs
import confluent.consoleserver as consoleserver
import confluent.forwarder as forwarder
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages
@@ -151,6 +152,7 @@ def _sessioncleaner():
if httpsessions[session]['expiry'] < currtime:
targsessions.append(session)
for session in targsessions:
forwarder.close_session(session)
del httpsessions[session]
targsessions = []
for session in consolesessions:
@@ -215,11 +217,17 @@ def _should_skip_authlog(env):
return True
return False
def _csrf_exempt(path):
# first a get of info to get CSRF key, also '/forward/web' to enable
# the popup ability to just forward
return path == '/sessions/current/info' or path.endswith('/forward/web')
def _csrf_valid(env, session):
# This could be simplified into a statement, but this is more readable
# to have it broken out
if (env['REQUEST_METHOD'] == 'GET' and
env['PATH_INFO'] == '/sessions/current/info'):
if env['REQUEST_METHOD'] == 'GET' and _csrf_exempt(env['PATH_INFO']):
# Provide a web client a safe hook to request the CSRF token
# This means that we consider GET of /sessions/current/info to be
# a safe thing to inflict via CSRF, since CORS should prevent
@@ -276,6 +284,7 @@ def _authorize_request(env, operation):
targets.append(mythread)
for mythread in targets:
eventlet.greenthread.kill(mythread)
forwarder.close_session(sessionid)
del httpsessions[sessionid]
return ('logout',)
httpsessions[sessionid]['expiry'] = time.time() + 90
@@ -391,7 +400,7 @@ def resourcehandler_backend(env, start_response):
('Pragma', 'no-cache'),
('X-Content-Type-Options', 'nosniff'),
('Content-Security-Policy', "default-src 'self'"),
('X-XSS-Protection', '1'), ('X-Frame-Options', 'deny'),
('X-XSS-Protection', '1; mode=block'), ('X-Frame-Options', 'deny'),
('Strict-Transport-Security', 'max-age=86400'),
('X-Permitted-Cross-Domain-Policies', 'none')]
reqbody = None
@@ -446,6 +455,24 @@ def resourcehandler_backend(env, start_response):
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
headers)
yield e.get_error_body()
elif (env['PATH_INFO'].endswith('/forward/web') and
env['PATH_INFO'].startswith('/nodes/')):
prefix, _, _ = env['PATH_INFO'].partition('/forward/web')
_, _, nodename = prefix.rpartition('/')
hm = cfgmgr.get_node_attributes(nodename, 'hardwaremanagement.manager')
targip = hm.get(nodename, {}).get(
'hardwaremanagement.manager', {}).get('value', None)
if not targip:
start_response('404 Not Found', headers)
yield 'No hardwaremanagemnet.manager defined for node'
return
funport = forwarder.get_port(targip, env['HTTP_X_FORWARDED_FOR'],
authorized['sessionid'])
host = env['HTTP_X_FORWARDED_HOST']
url = 'https://{0}:{1}/'.format(host, funport)
start_response('302', [('Location', url)])
yield 'Our princess is in another castle!'
return
elif (operation == 'create' and ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO'])):
#hard bake JSON into this path, do not support other incarnations
@@ -714,7 +741,10 @@ def _assemble_json(responses, resource=None, url=None, extension=None):
for dk in rsp.iterkeys():
if dk in rspdata:
if isinstance(rspdata[dk], list):
rspdata[dk].append(rsp[dk])
if isinstance(rsp[dk], list):
rspdata[dk].extend(rsp[dk])
else:
rspdata[dk].append(rsp[dk])
else:
rspdata[dk] = [rspdata[dk], rsp[dk]]
else:
@@ -755,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):
+20 -9
View File
@@ -379,7 +379,7 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
def _sizeRoll(self):
self.close()
for i in range(self.sizeRollingCount, 0, -1):
for i in range(self.backupCount - 1, 0, -1):
sbfn = "%s.%d" % (self.binpath, i)
dbfn = "%s.%d" % (self.binpath, i + 1)
stfn = "%s.%d" % (self.textpath, i)
@@ -392,8 +392,6 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
if os.path.exists(dtfn):
os.remove(dtfn)
os.rename(stfn, dtfn)
# size rolling happens, add statistics count
self.sizeRollingCount += 1
dbfn = self.binpath + ".1"
dtfn = self.textpath + ".1"
if os.path.exists(dbfn):
@@ -404,8 +402,18 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
os.rename(self.textpath, dtfn)
self._deleteFilesForSizeRolling()
return dbfn, dtfn
def _deleteFilesForSizeRolling(self):
for i in range(self.sizeRollingCount, self.backupCount -1, -1):
dbfn = "%s.%d" % (self.binpath, i)
dtfn = "%s.%d" % (self.textpath, i)
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
def _timeRoll(self):
self.close()
# get the time that this sequence started at and make it a TimeTuple
@@ -424,8 +432,7 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
addend = -3600
timeTuple = time.localtime(t + addend)
# if size rolling files exist
for i in range(self.sizeRollingCount, 0, -1):
for i in range(self.backupCount - 1, 0, -1):
sbfn = "%s.%d" % ( self.binpath, i)
dbfn = "%s.%s.%d" % (
self.binpath, time.strftime(self.suffix, timeTuple),i)
@@ -441,19 +448,17 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
os.remove(dtfn)
os.rename(stfn, dtfn)
# As time rolling happens, reset statistics count
self.sizeRollingCount = 0
dbfn = self.binpath + "." + time.strftime(self.suffix, timeTuple)
odbfn = dbfn
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
odtfn = dtfn
append=1
while os.path.exists(dbfn):
dbfn = odbfn + '.{}'.format(append)
dbfn = odbfn + '.{0}'.format(append)
append += 1
append=1
while os.path.exists(dtfn):
dtfn = odtfn + '.{}'.format(append)
dtfn = odtfn + '.{0}'.format(append)
append += 1
if os.path.exists(self.binpath):
os.rename(self.binpath, dbfn)
@@ -540,6 +545,12 @@ class Logger(object):
tstamp = entry[1]
data = entry[2]
evtdata = entry[3]
if len(data) > 65535:
# our max log entry is 65k, take only the first 65k and put
# rest back on as a continuation
entry[2] = data[65535:]
self.logentries.appendleft(entry)
data = data[:65535]
textdate = ''
if self.isconsole and ltype != 2:
textdate = time.strftime(
+13 -5
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:
@@ -163,7 +164,7 @@ def dumptrace(signalname, frame):
continue
if not o:
continue
ht.write('Thread trace:\n')
ht.write('Thread trace: ({0})\n'.format(id(o)))
ht.write(''.join(traceback.format_stack(o.gr_frame)))
ht.close()
@@ -204,7 +205,10 @@ def setlimits():
def run():
setlimits()
signal.signal(signal.SIGUSR1, dumptrace)
try:
signal.signal(signal.SIGUSR1, dumptrace)
except AttributeError:
pass # silly windows
if havefcntl:
_checkpidfile()
conf.init_config()
@@ -225,15 +229,19 @@ def run():
_updatepidfile()
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
collective.startup()
if dbgif:
oumask = os.umask(0077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass # We are not expecting the file to exist
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
try:
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
except AttributeError:
pass # Windows...
os.umask(oumask)
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
+162 -36
View File
@@ -19,6 +19,7 @@
# Things are defined here to 'encourage' developers to coordinate information
# format. This is also how different data formats are supported
import confluent.exceptions as exc
import confluent.config.configmanager as cfm
from copy import deepcopy
from datetime import datetime
import json
@@ -34,7 +35,7 @@ valid_health_values = set([
def _htmlify_structure(indict):
ret = "<ul>"
if isinstance(indict, dict):
for key in indict.iterkeys():
for key in sorted(indict):
ret += "<li>{0}: ".format(key)
if type(indict[key]) in (str, unicode, float, int):
ret += str(indict[key])
@@ -76,7 +77,7 @@ class ConfluentMessage(object):
datasource = self.kvpairs
else:
datasource = {'databynode': self.kvpairs}
jsonsnippet = json.dumps(datasource, separators=(',', ':'))[1:-1]
jsonsnippet = json.dumps(datasource, sort_keys=True, separators=(',', ':'))[1:-1]
return jsonsnippet
def raw(self):
@@ -138,7 +139,10 @@ class ConfluentMessage(object):
'<input type="checkbox" name="restexplorerhonorkey" '
'value="{1}">\r').format(valtype, key, self.desc)
return snippet
if val is not None and 'value' in val:
if (isinstance(val, bool) or isinstance(val, str) or
isinstance(val, unicode)):
value = str(val)
elif val is not None and 'value' in val:
value = val['value']
if 'inheritedfrom' in val:
notes.append('Inherited from %s' % val['inheritedfrom'])
@@ -162,7 +166,7 @@ class ConfluentMessage(object):
notes.append('Inherited from %s' % val['inheritedfrom'])
value = '********'
if self.readonly:
snippet += "{0}: {1}".format(key, value)
snippet += "{0}: {1}<br>".format(key, value)
else:
snippet += (key + ":" +
'<input type="{0}" name="{1}" value="{2}" '
@@ -243,13 +247,25 @@ class DeletedResource(ConfluentMessage):
def __init__(self, resource):
self.kvpairs = {'deleted': resource}
def strip_node(self, node):
pass
class CreatedResource(ConfluentMessage):
notnode = True
readonly = True
def __init__(self, resource):
self.kvpairs = {'created': resource}
def strip_node(self, node):
pass
class AssignedResource(ConfluentMessage):
notnode = True
readonly = True
def __init__(self, resource):
self.kvpairs = {'assigned': resource}
@@ -351,9 +367,13 @@ class ChildCollection(LinkRelation):
# InputMCI
# InputDomainName
# InputNTPServer
def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
configmanager=None):
if path[0] == 'power' and path[1] == 'state' and operation != 'retrieve':
return InputPowerMessage(path, nodes, inputdata)
elif (path in (['power', 'reseat'], ['_enclosure', 'reseat_bay']) and
operation != 'retrieve'):
return InputReseatMessage(path, nodes, inputdata)
elif path == ['attributes', 'expression']:
return InputExpression(path, inputdata, nodes)
elif path[0] in ('attributes', 'users') and operation != 'retrieve':
@@ -377,9 +397,13 @@ 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)
return InputNetworkConfiguration(path, nodes, inputdata,
configmanager)
elif (path[:3] == ['configuration', 'management_controller', 'domain_name']
and operation != 'retrieve'):
return InputDomainName(path, nodes, inputdata)
@@ -389,9 +413,41 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
'servers'] and operation != 'retrieve' and len(path) == 5):
return InputNTPServer(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'system', 'all'] and
operation != 'retrieve'):
return InputConfigChangeSet(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 inputdata:
raise exc.InvalidArgumentException()
raise exc.InvalidArgumentException(
'No known input handler for request')
class InputFirmwareUpdate(ConfluentMessage):
def __init__(self, path, nodes, inputdata):
self.filename = inputdata.get('filename', inputdata.get('url', None))
self.bank = inputdata.get('bank', None)
self.nodes = nodes
class InputMedia(InputFirmwareUpdate):
# Use InputFirmwareUpdate
pass
class DetachMedia(ConfluentMessage):
def __init__(self, path, nodes, inputdata):
if 'detachall' not in inputdata:
raise exc.InvalidArgumentException('Currently only supporting'
'{"detachall": 1}')
class Media(ConfluentMessage):
def __init__(self, node, media):
self.kvpairs = {node: {'name': media.name, 'url': media.url}}
class InputAlertData(ConfluentMessage):
@@ -420,23 +476,13 @@ class InputExpression(ConfluentMessage):
# so that it can make it intact to the pertinent configmanager function
def __init__(self, path, inputdata, nodes=None):
self.nodeattribs = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if nodes is None:
self.attribs = inputdata
return
for node in nodes:
if node in inputdata:
nestedmode = True
self.nodeattribs[node] = inputdata[node]
if nestedmode:
for key in inputdata:
if key not in nodes:
raise exc.InvalidArgumentException
else:
for node in nodes:
self.nodeattribs[node] = inputdata
self.nodeattribs[node] = inputdata
def get_attributes(self, node):
if node not in self.nodeattribs:
@@ -444,6 +490,27 @@ class InputExpression(ConfluentMessage):
nodeattr = deepcopy(self.nodeattribs[node])
return nodeattr
class InputConfigChangeSet(InputExpression):
# For now, this is identical to InputExpression, later it may
# internalize formula expansion, but not now..
def __init__(self, path, inputdata, nodes=None, configmanager=None):
self.cfm = configmanager
super(InputConfigChangeSet, self).__init__(path, inputdata, nodes)
def get_attributes(self, node):
attrs = super(InputConfigChangeSet, self).get_attributes(node)
endattrs = {}
for attr in attrs:
origval = attrs[attr]
if isinstance(origval, str) or isinstance(origval, unicode):
origval = {'expression': origval}
if 'expression' not in origval:
endattrs[attr] = attrs[attr]
else:
endattrs[attr] = list(self.cfm.expand_attrib_expression(
[node], attrs[attr]))[0][1]
return endattrs
class InputAttributes(ConfluentMessage):
# This is particularly designed for attributes, where a simple string
@@ -451,14 +518,13 @@ class InputAttributes(ConfluentMessage):
# preserve the client provided expression for posterity, rather than
# immediate consumption.
# for things like node configuration or similar, a different class is
# appropriate since it nedes to immediately expand an expression.
# appropriate since it needs to immediately expand an expression.
# with that class, the 'InputExpression' and calling code in attributes.py
# might be deprecated in favor of the generic expression expander
# and a small function in attributes.py to reflect the expansion back
# to the client
def __init__(self, path, inputdata, nodes=None):
self.nodeattribs = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if nodes is None:
@@ -483,16 +549,7 @@ class InputAttributes(ConfluentMessage):
'expression': self.attribs[attrib]}
return
for node in nodes:
if node in inputdata:
nestedmode = True
self.nodeattribs[node] = inputdata[node]
if nestedmode:
for key in inputdata:
if key not in nodes:
raise exc.InvalidArgumentException
else:
for node in nodes:
self.nodeattribs[node] = inputdata
self.nodeattribs[node] = inputdata
def get_attributes(self, node):
if node not in self.nodeattribs:
@@ -529,7 +586,6 @@ class InputCredential(ConfluentMessage):
def __init__(self, path, inputdata, nodes=None):
self.credentials = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
@@ -604,7 +660,7 @@ class ConfluentInputMessage(ConfluentMessage):
if self.keyname not in datum:
raise exc.InvalidArgumentException(
'missing {0} argument'.format(self.keyname))
elif datum[self.keyname] not in self.valid_values:
elif not self.is_valid_key(datum[self.keyname]):
raise exc.InvalidArgumentException(
datum[self.keyname] + ' is not one of ' +
','.join(self.valid_values))
@@ -614,13 +670,15 @@ class ConfluentInputMessage(ConfluentMessage):
if self.keyname not in datum:
raise exc.InvalidArgumentException(
'missing {0} argument'.format(self.keyname))
elif datum[self.keyname] not in self.valid_values:
elif not self.is_valid_key(datum[self.keyname]):
raise exc.InvalidArgumentException(datum[self.keyname] +
' is not one of ' +
','.join(self.valid_values))
for node in nodes:
self.inputbynode[node] = datum[self.keyname]
def is_valid_key(self, key):
return key in self.valid_values
class InputIdentifyMessage(ConfluentInputMessage):
valid_values = set([
@@ -644,6 +702,16 @@ class InputPowerMessage(ConfluentInputMessage):
def powerstate(self, node):
return self.inputbynode[node]
class InputReseatMessage(ConfluentInputMessage):
valid_values = set([
'reseat',
])
keyname = 'reseat'
def is_valid_key(self, key):
return key in self.valid_values or isinstance(key, int)
class InputBMCReset(ConfluentInputMessage):
valid_values = set([
@@ -654,6 +722,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 = {}
@@ -675,7 +762,7 @@ class InputMCI(ConfluentInputMessage):
class InputNetworkConfiguration(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
def __init__(self, path, nodes, inputdata, configmanager=None):
self.inputbynode = {}
self.stripped = False
if not inputdata:
@@ -699,8 +786,27 @@ class InputNetworkConfiguration(ConfluentInputMessage):
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
nodeattrmap = {}
for attr in inputdata:
try:
if inputdata[attr] is not None:
inputdata[attr].format()
except (KeyError, IndexError):
nodeattrmap[attr] = {}
for expanded in configmanager.expand_attrib_expression(
nodes, inputdata[attr]):
node, value = expanded
nodeattrmap[attr][node] = value
if not nodeattrmap:
for node in nodes:
self.inputbynode[node] = inputdata
return
# an expression was encountered
for node in nodes:
self.inputbynode[node] = inputdata
self.inputbynode[node] = deepcopy(inputdata)
for attr in self.inputbynode[node]:
if attr in nodeattrmap:
self.inputbynode[node][attr] = nodeattrmap[attr][node]
def netconfig(self, node):
return self.inputbynode[node]
@@ -855,6 +961,13 @@ class IdentifyState(ConfluentChoiceMessage):
keyname = 'identify'
class ReseatResult(ConfluentChoiceMessage):
valid_values = set([
'success',
])
keyname = 'reseat'
class PowerState(ConfluentChoiceMessage):
valid_values = set([
'on',
@@ -1170,7 +1283,7 @@ class Attributes(ConfluentMessage):
self.desc = desc
nkv = {}
self.notnode = name is None
for key in kv.iterkeys():
for key in kv:
if type(kv[key]) in (str, unicode):
nkv[key] = {'value': kv[key]}
else:
@@ -1182,6 +1295,8 @@ class Attributes(ConfluentMessage):
name: nkv
}
class ConfigSet(Attributes):
pass
class ListAttributes(ConfluentMessage):
def __init__(self, name=None, kv=None, desc=''):
@@ -1205,6 +1320,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
+27 -2
View File
@@ -16,6 +16,7 @@
# this will implement noderange grammar
import confluent.exceptions as exc
import codecs
import struct
import eventlet.green.socket as socket
@@ -83,7 +84,7 @@ def get_nic_config(configmanager, node, ip=None, mac=None):
cfgdata['prefix'] = prefixlen
for setting in nodenetattribs:
gw = nodenetattribs[setting].get('value', None)
if gw is None:
if gw is None or not gw:
continue
if ip_on_same_subnet(ip, gw, prefixlen):
cfgdata['ipv4_gateway'] = gw
@@ -121,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
+234 -15
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
# Copyright 2016, 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -30,13 +30,24 @@
# Provides support for viewing and processing lldp data for switches
if __name__ == '__main__':
import sys
import confluent.config.configmanager as cfm
import base64
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages as msg
import confluent.snmputil as snmp
import confluent.networking.netutil as netutil
import confluent.util as util
import eventlet
from eventlet.greenpool import GreenPool
import eventlet.semaphore
import re
# The interesting OIDs are:
# lldpLocChassisId - to cross reference (1.0.8802.1.1.2.1.3.2.0)
# lldpLocPortId - for cross referencing.. (1.0.8802.1.1.2.1.3.7.1.3)
# 1.0.8802.1.1.2.1.3.7.1.4 - Lookup of LLDP index id to description
# Yet another fun fact, the LLDP port index frequent
# does *not* map to ifName, like a sane
@@ -70,57 +81,184 @@ import re
# # 1.0.8802.1.1.2.1.4.1.1.10 - SysDesc - good stuff
_neighdata = {}
_neighbypeerid = {}
_updatelocks = {}
_chassisidbyswitch = {}
def lenovoname(idx, desc):
if desc.isdigit():
return 'Ethernet' + str(idx)
return 'Ethernet' + str(desc)
return desc
nameoverrides = [
(re.compile('20301\..*'), lenovoname),
]
# Lenovo chassis id rule is match only first 5 bytes for a match.....
def _api_sanitize_string(source):
source = source.strip()
return source.replace(':', '-').replace('/', '-')
def close_enough(fuzz, literal):
if fuzz == literal:
return True
fuzz = '^' + fuzz.replace('-', '[/: -]') + '$'
matcher = re.compile(fuzz)
return bool(matcher.match(literal))
def _lldpdesc_to_ifname(switchid, idx, desc):
for tform in nameoverrides:
if tform[0].match(switchid):
desc = tform[1](idx, desc)
return desc
return desc.strip().strip('\x00')
def _dump_neighbordatum(info):
return [msg.KeyValueData(info)]
def b64tohex(b64str):
bd = base64.b64decode(b64str)
return ''.join(['{0:02x}'.format(ord(x)) for x in bd])
def get_fingerprint(switch, port, configmanager, portmatch):
update_switch_data(switch, configmanager)
for neigh in _neighbypeerid:
info = _neighbypeerid[neigh]
if neigh == '!!vintage' or info.get('switch', None) != switch:
continue
if 'peersha256fingerprint' not in info:
continue
if info.get('switch', None) != switch:
continue
if portmatch(info.get('port'), port):
return ('sha256$' + b64tohex(info['peersha256fingerprint']),
info.get('verified', False))
return None, False
def _extract_extended_desc(info, source, integritychecked):
source = str(source)
info['verified'] = bool(integritychecked)
if source.startswith('Lenovo SMM;'):
info['peerdescription'] = 'Lenovo SMM'
if ';S2=' in source:
info['peersha256fingerprint'] = source.replace('Lenovo SMM;S2=',
'')
else:
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:
val = ':'.join(['{0:02x}'.format(ord(x)) for x in str(val)])
break
return val
def _init_lldp(data, iname, idx, idxtoportid, switch):
if iname not in data:
data[iname] = {'port': iname, 'portid': str(idxtoportid[idx]),
'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 = args
switch, password, user, force = args
vintage = _neighdata.get(switch, {}).get('!!vintage', 0)
now = util.monotonic_time()
if vintage > (now - 60) and not force:
return
conn = snmp.Session(switch, password, user)
sid = None
lldpdata = {}
lldpdata = {'!!vintage': now}
for sysid in conn.walk('1.3.6.1.2.1.1.2'):
sid = str(sysid[1][6:])
idxtoifname = {}
idxtoportid = {}
_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])
for oidindex in conn.walk('1.0.8802.1.1.2.1.3.7.1.4'):
idx = oidindex[0][-1]
idxtoifname[idx] = _lldpdesc_to_ifname(sid, idx, str(oidindex[1]))
for remotedesc in conn.walk('1.0.8802.1.1.2.1.4.1.1.10'):
iname = idxtoifname[remotedesc[0][-2]]
lldpdata[iname] = {'description': str(remotedesc[1])}
_init_lldp(lldpdata, iname, remotedesc[0][-2], idxtoportid, switch)
_extract_extended_desc(lldpdata[iname], remotedesc[1], user)
for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.9'):
iname = idxtoifname[remotename[0][-2]]
if iname not in lldpdata:
lldpdata[iname] = {}
lldpdata[iname]['name'] = str(remotename[1])
_init_lldp(lldpdata, iname, remotename[0][-2], idxtoportid, switch)
lldpdata[iname]['peername'] = str(remotename[1])
for remotename in conn.walk('1.0.8802.1.1.2.1.4.1.1.7'):
iname = idxtoifname[remotename[0][-2]]
_init_lldp(lldpdata, iname, remotename[0][-2], idxtoportid, switch)
lldpdata[iname]['peerportid'] = sanitize(remotename[1])
for remoteid in conn.walk('1.0.8802.1.1.2.1.4.1.1.5'):
iname = idxtoifname[remoteid[0][-2]]
if iname not in lldpdata:
lldpdata[iname] = {}
lldpdata[iname]['chassisid'] = str(remoteid[1])
print(repr(lldpdata))
_init_lldp(lldpdata, iname, remoteid[0][-2], idxtoportid, switch)
lldpdata[iname]['peerchassisid'] = sanitize(remoteid[1])
for entry in lldpdata:
if entry == '!!vintage':
continue
entry = lldpdata[entry]
entry['switch'] = switch
peerid = '{0}.{1}'.format(
entry.get('peerchassisid', '').replace(':', '-').replace('/', '-'),
entry.get('peerportid', '').replace(':', '-').replace('/', '-'))
entry['peerid'] = peerid
_neighbypeerid[peerid] = entry
_neighdata[switch] = lldpdata
def update_switch_data(switch, configmanager, force=False):
switchcreds = netutil.get_switchcreds(configmanager, (switch,))[0]
_extract_neighbor_data(switchcreds + (force,))
return _neighdata.get(switch, {})
def update_neighbors(configmanager, force=False):
return _update_neighbors_backend(configmanager, force)
def _update_neighbors_backend(configmanager, force):
global _neighdata
global _neighbypeerid
vintage = _neighdata.get('!!vintage', 0)
now = util.monotonic_time()
if vintage > (now - 60) and not force:
return
_neighdata = {'!!vintage': now}
_neighbypeerid = {'!!vintage': now}
switches = netutil.list_switches(configmanager)
switchcreds = netutil.get_switchcreds(configmanager, switches)
switchcreds = [ x + (force,) for x in switchcreds]
pool = GreenPool(64)
for ans in pool.imap(_extract_neighbor_data, switchcreds):
yield ans
def _extract_neighbor_data(args):
# single switch neighbor data update
switch = args[0]
if switch not in _updatelocks:
_updatelocks[switch] = eventlet.semaphore.Semaphore()
if _updatelocks[switch].locked():
while _updatelocks[switch].locked():
eventlet.sleep(1)
return
try:
_extract_neighbor_data_b(args)
with _updatelocks[switch]:
_extract_neighbor_data_b(args)
except Exception:
log.logtrace()
@@ -128,4 +266,85 @@ if __name__ == '__main__':
# a quick one-shot test, args are switch and snmpv1 string for now
# (should do three argument form for snmpv3 test
import sys
_extract_neighbor_data((sys.argv[1], sys.argv[2]))
_extract_neighbor_data((sys.argv[1], sys.argv[2], None, True))
print(repr(_neighdata))
multi_selectors = set(['by-switch', 'by-peername', 'by-peerportid',
'by-peerchassisid', 'by-chassisid', 'by-port',
'by-portid'])
single_selectors = set(['by-peerid'])
def _parameterize_path(pathcomponents):
listrequested = False
childcoll = True
if len(pathcomponents) % 2 == 1:
listrequested = pathcomponents[-1]
pathcomponents = pathcomponents[:-1]
pathit = iter(pathcomponents)
keyparams = {}
validselectors = multi_selectors | single_selectors
for key, val in zip(pathit, pathit):
if key not in validselectors:
raise exc.NotFoundException('{0} is not valid here'.format(key))
keyparams[key] = val
validselectors.discard(key)
if key == 'by-switch':
validselectors.add('by-port')
if key in single_selectors:
childcoll = False
validselectors = set([])
return validselectors, keyparams, listrequested, childcoll
def list_info(parms, requestedparameter):
#{u'by-switch': u'r8e1', u'by-port': u'e'}
#by-peerport
suffix = '/' if requestedparameter in multi_selectors else ''
results = set([])
requestedparameter = requestedparameter.replace('by-', '')
for info in _neighbypeerid:
if info == '!!vintage':
continue
info = _neighbypeerid[info]
for mk in parms:
mk = mk.replace('by-', '')
if mk not in info:
continue
if (not close_enough(parms['by-' + mk], info[mk]) or
requestedparameter not in info):
break
else:
candidate = info[requestedparameter]
candidate = candidate.strip()
if candidate != '':
results.add(_api_sanitize_string(candidate))
return [msg.ChildCollection(x + suffix) for x in util.natural_sort(results)]
def _handle_neighbor_query(pathcomponents, configmanager):
choices, parms, listrequested, childcoll = _parameterize_path(
pathcomponents)
if not childcoll: # this means it's a single entry with by-peerid
# guaranteed
if (parms['by-peerid'] not in _neighbypeerid and
_neighbypeerid.get('!!vintage', 0) < util.monotonic_time() - 60):
list(update_neighbors(configmanager))
if parms['by-peerid'] not in _neighbypeerid:
raise exc.NotFoundException('No matching peer known')
return _dump_neighbordatum(_neighbypeerid[parms['by-peerid']])
if not listrequested: # the query is for currently valid choices
return [msg.ChildCollection(x + '/') for x in sorted(list(choices))]
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)
else:
list(update_neighbors(configmanager))
return list_info(parms, listrequested)
def _list_interfaces(switchname, configmanager):
switchcreds = get_switchcreds(configmanager, (switchname,))
switchcreds = switchcreds[0]
conn = snmp.Session(*switchcreds)
ifnames = netutil.get_portnamemap(conn)
return util.natural_sort(ifnames.values())
+69 -56
View File
@@ -30,6 +30,8 @@
# this module will provide mac to switch and full 'ifName' label
# This functionality is restricted to the null tenant
from confluent.networking.lldp import _handle_neighbor_query, get_fingerprint
from confluent.networking.netutil import get_switchcreds, list_switches, get_portnamemap
if __name__ == '__main__':
import sys
@@ -48,6 +50,7 @@ _macmap = {}
_macsbyswitch = {}
_nodesbymac = {}
_switchportmap = {}
_neighdata = {}
vintage = None
@@ -179,20 +182,7 @@ def _map_switch_backend(args):
except ValueError:
# ifidx might be '', skip in such a case
continue
ifnamemap = {}
havenames = False
for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
ifidx, ifname = vb
if not ifname:
continue
havenames = True
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
if not havenames:
for vb in conn.walk( '1.3.6.1.2.1.2.2.1.2'):
ifidx, ifname = vb
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
ifnamemap = get_portnamemap(conn)
maccounts = {}
bridgetoifvalid = False
for mac in mactobridge:
@@ -239,29 +229,34 @@ def _map_switch_backend(args):
_macsbyswitch[switch][ifname] = [mac]
nodename = _nodelookup(switch, ifname)
if nodename is not None:
if mac in _nodesbymac and _nodesbymac[mac] != nodename:
if mac in _nodesbymac and _nodesbymac[mac][0] != nodename:
# For example, listed on both a real edge port
# and by accident a trunk port
log.log({'error': '{0} and {1} described by ambiguous'
' switch topology values'.format(nodename,
_nodesbymac[mac]
)})
_nodesbymac[mac] = None
' switch topology values'.format(
nodename, _nodesbymac[mac][0])})
_nodesbymac[mac] = (None, None)
else:
_nodesbymac[mac] = nodename
_nodesbymac[mac] = (nodename, maccounts[ifname])
def find_node_by_mac(mac, configmanager):
switchbackoff = 30
def find_nodeinfo_by_mac(mac, configmanager):
now = util.monotonic_time()
if vintage and (now - vintage) < 90 and mac in _nodesbymac:
return _nodesbymac[mac]
return _nodesbymac[mac][0], {'maccount': _nodesbymac[mac][1]}
# do not actually sweep switches more than once every 30 seconds
# however, if there is an update in progress, wait on it
for _ in update_macmap(configmanager, vintage and (now - vintage) < 30):
for _ in update_macmap(configmanager,
vintage and (now - vintage) < switchbackoff):
if mac in _nodesbymac:
return _nodesbymac[mac]
return _nodesbymac[mac][0], {'maccount': _nodesbymac[mac][1]}
# If update_mac bailed out, still check one last time
return _nodesbymac.get(mac, None)
if mac in _nodesbymac:
return _nodesbymac[mac][0], {'maccount': _nodesbymac[mac][1]}
return None, {'maccount': 0}
mapupdating = eventlet.semaphore.Semaphore()
@@ -292,16 +287,20 @@ def update_macmap(configmanager, impatient=False):
eventlet.spawn_n(_finish_update, completions)
raise
def _finish_update(completions):
for _ in completions:
pass
def _full_updatemacmap(configmanager):
global vintage
global _macmap
global _nodesbymac
global _switchportmap
global _macsbyswitch
global switchbackoff
start = util.monotonic_time()
with mapupdating:
vintage = util.monotonic_time()
# Clear all existing entries
@@ -312,6 +311,7 @@ def _full_updatemacmap(configmanager):
if configmanager.tenant is not None:
raise exc.ForbiddenRequest(
'Network topology not available to tenants')
# here's a list of switches... need to add nodes that are switches
nodelocations = configmanager.get_node_attributes(
configmanager.list_nodes(), ('net*.switch', 'net*.switchport'))
switches = set([])
@@ -340,28 +340,18 @@ def _full_updatemacmap(configmanager):
_switchportmap[curswitch][portname] = None
else:
_switchportmap[curswitch][portname] = node
switchcfg = configmanager.get_node_attributes(
switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity',
'secret.hardwaremanagementpassword'), decrypt=True)
switchauth = []
for switch in switches:
if not switch:
continue
switchparms = switchcfg.get(switch, {})
user = None
password = switchparms.get(
'secret.snmpcommunity', {}).get('value', None)
if not password:
password = switchparms.get(
'secret.hardwaremanagementpassword', {}).get('value',
'public')
user = switchparms.get(
'secret.hardwaremanagementuser', {}).get('value', None)
switchauth.append((switch, password, user))
pool = GreenPool()
switchauth = get_switchcreds(configmanager, switches)
pool = GreenPool(64)
for ans in pool.imap(_map_switch, switchauth):
vintage = util.monotonic_time()
yield ans
endtime = util.monotonic_time()
duration = endtime - start
duration = duration * 15 # wait 15 times as long as it takes to walk
# avoid spending a large portion of the time hitting switches with snmp
# requests
if duration > switchbackoff:
switchbackoff = duration
def _dump_locations(info, macaddr, nodename=None):
@@ -378,7 +368,7 @@ def _dump_locations(info, macaddr, nodename=None):
def handle_api_request(configmanager, inputdata, operation, pathcomponents):
if operation == 'retrieve':
return handle_read_api_request(pathcomponents)
return handle_read_api_request(pathcomponents, configmanager)
if (operation in ('update', 'create') and
pathcomponents == ['networking', 'macs', 'rescan']):
if inputdata != {'rescan': 'start'}:
@@ -390,20 +380,44 @@ def handle_api_request(configmanager, inputdata, operation, pathcomponents):
operation, '/'.join(pathcomponents)))
def handle_read_api_request(pathcomponents):
def get_node_fingerprints(nodename, configmanager):
cfg = configmanager.get_node_attributes(nodename, ['net*.switch',
'net*.switchport'])
for attrkey in cfg[nodename]:
if attrkey.endswith('switch'):
switch = cfg[nodename][attrkey]['value']
port = cfg[nodename][attrkey + 'port']['value']
yield get_fingerprint(switch, port, configmanager,
_namesmatch)
def handle_read_api_request(pathcomponents, configmanager):
# TODO(jjohnson2): discovery core.py api handler design, apply it here
# to make this a less tangled mess as it gets extended
if len(pathcomponents) == 1:
return [msg.ChildCollection('macs/')]
return [msg.ChildCollection('macs/'),
msg.ChildCollection('neighbors/')]
elif pathcomponents[1] == 'neighbors':
if len(pathcomponents) == 3 and pathcomponents[-1] == 'by-switch':
return [msg.ChildCollection(x + '/')
for x in list_switches(configmanager)]
else:
return _handle_neighbor_query(pathcomponents[2:], configmanager)
elif len(pathcomponents) == 2:
return [msg.ChildCollection(x) for x in (# 'by-node/',
'by-mac/', 'by-switch/',
'rescan')]
if pathcomponents[-1] == 'macs':
return [msg.ChildCollection(x) for x in (# 'by-node/',
'by-mac/', 'by-switch/',
'rescan')]
elif pathcomponents[-1] == 'neighbors':
return [msg.ChildCollection('by-switch/')]
else:
raise exc.NotFoundException(
'Unknown networking resource {0}'.format(pathcomponents[-1]))
if False and pathcomponents[2] == 'by-node':
# TODO: should be list of node names, and then under that 'by-mac'
if len(pathcomponents) == 3:
return [msg.ChildCollection(x.replace(':', '-'))
for x in sorted(list(_nodesbymac))]
for x in util.natural_sort(list(_nodesbymac))]
elif len(pathcomponents) == 4:
macaddr = pathcomponents[-1].replace('-', ':')
return dump_macinfo(macaddr)
@@ -416,8 +430,7 @@ def handle_read_api_request(pathcomponents):
elif pathcomponents[2] == 'by-switch':
if len(pathcomponents) == 3:
return [msg.ChildCollection(x + '/')
for x in sorted(list(_macsbyswitch))]
for x in list_switches(configmanager)]
if len(pathcomponents) == 4:
return [msg.ChildCollection('by-port/')]
if len(pathcomponents) == 5:
@@ -426,7 +439,7 @@ def handle_read_api_request(pathcomponents):
raise exc.NotFoundException(
'No known macs for switch {0}'.format(switchname))
return [msg.ChildCollection(x.replace('/', '-') + '/')
for x in sorted(list(_macsbyswitch[switchname]))]
for x in util.natural_sort(list(_macsbyswitch[switchname]))]
if len(pathcomponents) == 6:
return [msg.ChildCollection('by-mac/')]
if len(pathcomponents) == 7:
@@ -449,13 +462,13 @@ def handle_read_api_request(pathcomponents):
def dump_macinfo(macaddr):
macaddr = macaddr.replace('-', ':')
macaddr = macaddr.replace('-', ':').lower()
info = _macmap.get(macaddr, None)
if info is None:
raise exc.NotFoundException(
'{0} not found in mac table of '
'any known switches'.format(macaddr))
return _dump_locations(info, macaddr, _nodesbymac.get(macaddr, None))
return _dump_locations(info, macaddr, _nodesbymac.get(macaddr, (None,))[0])
def rescan(cfg):
@@ -0,0 +1,74 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.util as util
def get_switchcreds(configmanager, switches):
switchcfg = configmanager.get_node_attributes(
switches, ('secret.hardwaremanagementuser', 'secret.snmpcommunity',
'secret.hardwaremanagementpassword'), decrypt=True)
switchauth = []
for switch in switches:
if not switch:
continue
switchparms = switchcfg.get(switch, {})
user = None
password = switchparms.get(
'secret.snmpcommunity', {}).get('value', None)
if not password:
password = switchparms.get(
'secret.hardwaremanagementpassword', {}).get('value',
'public')
user = switchparms.get(
'secret.hardwaremanagementuser', {}).get('value', None)
if not user:
user = None
switchauth.append((switch, password, user))
return switchauth
def list_switches(configmanager):
nodelocations = configmanager.get_node_attributes(
configmanager.list_nodes(), ('net*.switch', 'net*.switchport'))
switches = set([])
for node in nodelocations:
cfg = nodelocations[node]
for attr in cfg:
if not attr.endswith('.switch') or 'value' not in cfg[attr]:
continue
curswitch = cfg[attr].get('value', None)
if not curswitch:
continue
switches.add(curswitch)
return util.natural_sort(switches)
def get_portnamemap(conn):
ifnamemap = {}
havenames = False
for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
ifidx, ifname = vb
if not ifname:
continue
havenames = True
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
if not havenames:
for vb in conn.walk('1.3.6.1.2.1.2.2.1.2'):
ifidx, ifname = vb
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
return ifnamemap
+41
View File
@@ -38,6 +38,9 @@ _parser = pp.nestedExpr(content=_grammar)
_numextractor = pp.OneOrMore(pp.Word(pp.alphas + '-') | pp.Word(pp.nums))
numregex = re.compile('([0-9]+)')
lastnoderange = None
def humanify_nodename(nodename):
"""Analyzes nodename in a human way to enable natural sort
@@ -48,6 +51,42 @@ def humanify_nodename(nodename):
for text in re.split(numregex, nodename)]
class ReverseNodeRange(object):
"""Abbreviate a set of nodes to a shorter noderange representation
:param nodes: List of nodes as a list, tuple, etc.
:param config: Config manager
"""
def __init__(self, nodes, config):
self.cfm = config
self.nodes = set(nodes)
@property
def noderange(self):
subsetgroups = []
for group in self.cfm.get_groups(sizesort=True):
if lastnoderange:
for nr in lastnoderange:
if lastnoderange[nr] - self.nodes:
continue
if self.nodes - lastnoderange[nr]:
continue
return nr
nl = set(
self.cfm.get_nodegroup_attributes(group).get('nodes', []))
if len(nl) > len(self.nodes) or not nl:
continue
if not nl - self.nodes:
subsetgroups.append(group)
self.nodes -= nl
if not self.nodes:
break
return ','.join(sorted(subsetgroups) + sorted(self.nodes))
# TODO: pagination operators <pp.nums and >pp.nums for begin and end respective
class NodeRange(object):
"""Iterate over a noderange
@@ -57,6 +96,7 @@ class NodeRange(object):
"""
def __init__(self, noderange, config=None):
global lastnoderange
self.beginpage = None
self.endpage = None
self.cfm = config
@@ -70,6 +110,7 @@ class NodeRange(object):
self._noderange = set(self.cfm.list_nodes())
else:
self._noderange = self._evaluate(elements)
lastnoderange = {noderange: set(self._noderange)}
@property
def nodes(self):
@@ -1,4 +1,5 @@
# Copyright 2014 IBM Corporation
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,9 +16,11 @@
import confluent.exceptions as exc
import confluent.messages as msg
import confluent.config.attributes as allattributes
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':
@@ -26,7 +29,13 @@ def retrieve(nodes, element, configmanager, inputdata):
def retrieve_nodegroup(nodegroup, element, configmanager, inputdata):
grpcfg = configmanager.get_nodegroup_attributes(nodegroup)
try:
grpcfg = configmanager.get_nodegroup_attributes(nodegroup)
except KeyError:
if not configmanager.is_nodegroup(nodegroup):
raise exc.NotFoundException(
'Invalid nodegroup: {0} not found'.format(nodegroup))
raise
if element == 'all':
theattrs = set(allattributes.node).union(set(grpcfg))
theattrs.add('nodes')
@@ -168,8 +177,18 @@ def _expand_expression(nodes, configmanager, inputdata):
expression = expression['expression']
if type(expression) is dict:
expression = expression['expression']
for expanded in configmanager.expand_attrib_expression(nodes, expression):
yield msg.KeyValueData({'value': expanded[1]}, expanded[0])
pernodeexpressions = {}
try:
for expanded in configmanager.expand_attrib_expression(nodes,
expression):
pernodeexpressions[expanded[0]] = expanded[1]
for node in util.natural_sort(pernodeexpressions):
yield msg.KeyValueData({'value': pernodeexpressions[node]}, node)
except (SyntaxError, ValueError) as e:
raise exc.InvalidArgumentException(
'Bad confluent expression syntax (must use "{{" and "}}" if not '
'desiring confluent expansion): ' + str(e))
def create(nodes, element, configmanager, inputdata):
@@ -0,0 +1,38 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.core as core
import confluent.messages as msg
import pyghmi.exceptions as pygexc
def update(nodes, element, configmanager, inputdata):
emebs = configmanager.get_node_attributes(
nodes, (u'enclosure.manager', u'enclosure.bay'))
for node in nodes:
try:
em = emebs[node]['enclosure.manager']['value']
eb = emebs[node]['enclosure.bay']['value']
except KeyError:
yield msg.ConfluentNodeError(
node,
'Reseat is only supported on servers in an enclosure, and '
'with enclosure.manager and enclosure.bay defined')
continue
try:
for rsp in core.handle_path(
'/nodes/{0}/_enclosure/reseat_bay'.format(em),
'update', configmanager,
inputdata={'reseat': int(eb)}):
yield rsp
except pygexc.UnsupportedFunctionality as uf:
yield msg.ConfluentNodeError(node, str(uf))
@@ -1,5 +1,5 @@
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
import atexit
import confluent.exceptions as exc
import confluent.firmwaremanager as firmwaremanager
import confluent.interface.console as conapi
import confluent.messages as msg
import confluent.util as util
@@ -29,9 +30,30 @@ import pyghmi.exceptions as pygexc
console = eventlet.import_patched('pyghmi.ipmi.console')
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
import socket
import ssl
# There is something not right with the RLocks used in pyghmi when
# eventlet comes into play. It seems like sometimes on acquire,
# it calls _get_ident and it isn't the id(greenlet) and so
# a thread deadlocks itself due to identity crisis?
# However, since we are not really threaded, the operations being protected
# are not actually dangerously multiplexed... so we can replace with
# a null context manager for now
class NullLock(object):
def donothing(self, *args, **kwargs):
return 1
__enter__ = donothing
__exit__ = donothing
acquire = donothing
release = donothing
console.session.select = eventlet.green.select
console.session.threading = eventlet.green.threading
console.session.WAITING_SESSIONS = NullLock()
console.session.KEEPALIVE_SESSIONS = NullLock()
console.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
@@ -48,7 +70,8 @@ _ipmiwaiters = []
sensor_categories = {
'temperature': frozenset(['Temperature']),
'power': frozenset(['Current', 'Battery']),
'energy': frozenset(['Energy']),
'power': frozenset(['Power', 'Current']),
'fans': frozenset(['Fan', 'Cooling Device']),
}
@@ -109,10 +132,11 @@ 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)
self.setup_confluent_keyhandler()
def setup_confluent_keyhandler(self):
self.register_key_handler(util.TLSCertVerifier(
@@ -245,13 +269,18 @@ class IpmiConsole(conapi.Console):
password=self.password,
kg=self.kg, force=True,
iohandler=self.handle_data)
while not self.solconnection.connected and not self.broken:
self.solconnection.outputlock = NullLock()
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
@@ -267,6 +296,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"
@@ -293,6 +323,8 @@ def perform_requests(operator, nodes, element, cfg, inputdata):
datum = resultdata.get(timeout=10)
while datum:
if datum != 'Done':
if isinstance(datum, Exception):
raise datum
yield datum
datum = resultdata.get_nowait()
except queue.Empty:
@@ -317,16 +349,26 @@ 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))
raise
except exc.TargetEndpointUnreachable as tu:
results.put(msg.ConfluentTargetTimeout(node, str(tu)))
except Exception as e:
except ssl.SSLEOFError:
results.put(msg.ConfluentNodeError(
node, 'IPMI PluginException (see stderr log): ' + str(e)))
self.node, 'Unable to communicate with the https server on '
'the target BMC'))
except exc.PubkeyInvalid:
results.put(msg.ConfluentNodeError(
node,
'Mismatch detected between target certificate fingerprint '
'and pubkeys.tls_hardwaremanager attribute'))
except pygexc.InvalidParameterValue as e:
results.put(msg.ConfluentNodeError(node, str(e)))
except Exception as e:
results.put(e)
raise
finally:
results.put('Done')
@@ -350,9 +392,11 @@ class IpmiHandler(object):
connparams = get_conn_params(node, self.cfg)
self.ipmicmd = None
self.inputdata = inputdata
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
@@ -363,12 +407,14 @@ class IpmiHandler(object):
userid=connparams['username'],
password=connparams['passphrase'], kg=connparams['kg'],
port=connparams['port'], onlogon=self.logged)
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:
@@ -388,7 +434,6 @@ class IpmiHandler(object):
else:
self.ipmicmd = ipmicmd
self.loggedin = True
self.ipmicmd.setup_confluent_keyhandler()
def handle_request(self):
if self.broken:
@@ -407,6 +452,8 @@ class IpmiHandler(object):
raise Exception(self.error)
if self.element == ['power', 'state']:
self.power()
elif self.element == ['_enclosure', 'reseat_bay']:
self.reseat_bay()
elif self.element == ['boot', 'nextdevice']:
self.bootdevice()
elif self.element == ['health', 'hardware']:
@@ -417,8 +464,18 @@ class IpmiHandler(object):
self.handle_sensors()
elif self.element[0] == 'configuration':
self.handle_configuration()
elif self.element[:3] == ['inventory', 'firmware', 'updates']:
self.handle_update()
elif self.element[0] == 'inventory':
self.handle_inventory()
elif self.element == ['media', 'attach']:
self.handle_attach_media()
elif self.element == ['media', 'detach']:
self.handle_detach_media()
elif self.element == ['media', 'uploads']:
self.handle_media_upload()
elif self.element == ['media', 'current']:
self.handle_list_media()
elif self.element == ['events', 'hardware', 'log']:
self.do_eventlog()
elif self.element == ['events', 'hardware', 'decode']:
@@ -428,6 +485,35 @@ class IpmiHandler(object):
else:
raise Exception('Not Implemented')
def handle_update(self):
u = firmwaremanager.Updater(self.node, self.ipmicmd.update_firmware,
self.inputdata.filename, self.tenant,
bank=self.inputdata.bank)
self.output.put(
msg.CreatedResource(
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
self.node, u.name)))
def handle_media_upload(self):
u = firmwaremanager.Updater(self.node, self.ipmicmd.upload_media,
self.inputdata.filename, self.tenant,
type='mediaupload')
self.output.put(msg.CreatedResource(
'nodes/{0}/media/uploads/{1}'.format(self.node, u.name)))
def handle_attach_media(self):
try:
self.ipmicmd.attach_remote_media(self.inputdata.filename)
except pygexc.UnsupportedFunctionality as uf:
self.output.put(msg.ConfluentNodeError(self.node, str(uf)))
def handle_detach_media(self):
self.ipmicmd.detach_remote_media()
def handle_list_media(self):
for media in self.ipmicmd.list_media():
self.output.put(msg.Media(self.node, media))
def handle_configuration(self):
if self.element[1:3] == ['management_controller', 'alerts']:
return self.handle_alerts()
@@ -439,10 +525,14 @@ 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()
raise Exception('Not implemented')
def decode_alert(self):
@@ -514,10 +604,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
@@ -665,10 +765,27 @@ class IpmiHandler(object):
def read_firmware(self, component):
items = []
for id, data in self.ipmicmd.get_firmware():
if component == 'all' or component == simplify_name(id):
items.append({id: data})
errorneeded = False
try:
complist = () if component == 'all' else (component,)
for id, data in self.ipmicmd.get_firmware(complist):
if (component in ('core', 'all') or
component == simplify_name(id)):
items.append({id: data})
except ssl.SSLEOFError:
errorneeded = msg.ConfluentNodeError(
self.node, 'Unable to communicate with the https server on '
'the target BMC while trying to read extended '
'information')
except exc.PubkeyInvalid:
errorneeded = msg.ConfluentNodeError(
self.node,
'Extended information unavailable, mismatch detected between '
'target certificate fingerprint and '
'pubkeys.tls_hardwaremanager attribute')
self.output.put(msg.Firmware(items, self.node))
if errorneeded:
self.output.put(errorneeded)
def handle_inventory(self):
if self.element[1] == 'firmware':
@@ -696,32 +813,47 @@ class IpmiHandler(object):
self.output.put(msg.LEDStatus(led_categories, self.node))
def read_inventory(self, component):
invitems = []
if component == 'all':
for invdata in self.ipmicmd.get_inventory():
if invdata[1] is None:
errorneeded = False
try:
invitems = []
if component == 'all':
for invdata in self.ipmicmd.get_inventory():
if invdata[1] is None:
newinf = {'present': False, 'information': None}
else:
sanitize_invdata(invdata[1])
newinf = {'present': True, 'information': invdata[1]}
newinf['name'] = invdata[0]
invitems.append(newinf)
else:
self.make_inventory_map()
compname = self.invmap.get(component, None)
if compname is None:
self.output.put(msg.ConfluentTargetNotFound())
return
invdata = self.ipmicmd.get_inventory_of_component(compname)
if invdata is None:
newinf = {'present': False, 'information': None}
else:
sanitize_invdata(invdata[1])
newinf = {'present': True, 'information': invdata[1]}
newinf['name'] = invdata[0]
sanitize_invdata(invdata)
newinf = {'present': True, 'information': invdata}
newinf['name'] = compname
invitems.append(newinf)
else:
self.make_inventory_map()
compname = self.invmap.get(component, None)
if compname is None:
self.output.put(msg.ConfluentTargetNotFound())
return
invdata = self.ipmicmd.get_inventory_of_component(compname)
if invdata is None:
newinf = {'present': False, 'information': None}
else:
sanitize_invdata(invdata)
newinf = {'present': True, 'information': invdata}
newinf['name'] = compname
invitems.append(newinf)
except ssl.SSLEOFError:
errorneeded = msg.ConfluentNodeError(
self.node, 'Unable to communicate with the https server on '
'the target BMC while trying to read extended '
'information')
except exc.PubkeyInvalid:
errorneeded = msg.ConfluentNodeError(
self.node,
'Extended information unavailable, mismatch detected between '
'target certificate fingerprint and '
'pubkeys.tls_hardwaremanager attribute')
newinvdata = {'inventory': invitems}
self.output.put(msg.KeyValueData(newinvdata, self.node))
if errorneeded:
self.output.put(errorneeded)
def handle_sensors(self):
if self.element[-1] == '':
@@ -776,6 +908,14 @@ class IpmiHandler(object):
else:
raise exc.InvalidArgumentException('health is read-only')
def reseat_bay(self):
bay = self.inputdata.inputbynode[self.node]
try:
self.ipmicmd.reseat_bay(bay)
self.output.put(msg.ReseatResult(self.node, 'success'))
except pygexc.UnsupportedFunctionality as uf:
self.output.put(uf)
def bootdevice(self):
if 'read' == self.op:
bootdev = self.ipmicmd.get_bootdev()
@@ -865,6 +1005,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()
@@ -875,6 +1025,14 @@ class IpmiHandler(object):
self.ipmicmd.set_domain_name(dn)
return
def handle_sysconfig(self):
if 'read' == self.op:
self.output.put(msg.ConfigSet(
self.node, self.ipmicmd.get_system_configuration()))
elif 'update' == self.op:
self.ipmicmd.set_system_configuration(
self.inputdata.get_attributes(self.node))
def handle_ntp(self):
if self.element[3] == 'enabled':
if 'read' == self.op:
@@ -963,9 +1121,22 @@ def update(nodes, element, configmanager, inputdata):
def retrieve(nodes, element, configmanager, inputdata):
initthread()
return perform_requests('read', nodes, element, configmanager, inputdata)
if '/'.join(element).startswith('inventory/firmware/updates/active'):
return firmwaremanager.list_updates(nodes, configmanager.tenant,
element)
elif '/'.join(element).startswith('media/uploads'):
return firmwaremanager.list_updates(nodes, configmanager.tenant,
element, 'mediaupload')
else:
return perform_requests('read', nodes, element, configmanager, inputdata)
def delete(nodes, element, configmanager, inputdata):
initthread()
if '/'.join(element).startswith('inventory/firmware/updates/active'):
return firmwaremanager.remove_updates(nodes, configmanager.tenant,
element)
elif '/'.join(element).startswith('media/uploads'):
return firmwaremanager.remove_updates(nodes, configmanager.tenant,
element, type='mediaupload')
return perform_requests(
'delete', nodes, element, configmanager, inputdata)
@@ -63,6 +63,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
@@ -128,7 +129,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 +156,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
+7 -2
View File
@@ -59,8 +59,9 @@ class Session(object):
# SNMP v2c
self.authdata = snmp.CommunityData(secret, mpModel=1)
else:
self.authdata = snmp.UsmUserData(username, authKey=secret,
privKey=secret)
self.authdata = snmp.UsmUserData(
username, authKey=secret, privKey=secret,
authProtocol=snmp.usmHMACSHAAuthProtocol)
self.eng = snmp.SnmpEngine()
def walk(self, oid):
@@ -98,6 +99,10 @@ class Session(object):
elif errnum:
raise exc.ConfluentException(errnum.prettyPrint())
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')
+142 -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,18 @@ 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'])
term_interact(None, None, ccons, None, connection, consession, None)
def start_term(authname, cfm, connection, params, path, authdata, skipauth):
elems = path.split('/')
@@ -210,12 +263,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 +283,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 +304,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 +329,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 +435,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)
+65 -10
View File
@@ -22,6 +22,9 @@ import confluent.log as log
import hashlib
import netifaces
import os
import re
import socket
import ssl
import struct
@@ -31,13 +34,22 @@ def list_interface_indexes():
# For now be linux specific
try:
for iface in os.listdir('/sys/class/net/'):
if not os.path.exists('/sys/class/net/{0}/ifindex'.format(iface)):
continue
ifile = open('/sys/class/net/{0}/ifindex'.format(iface), 'r')
intidx = int(ifile.read())
ifile.close()
yield intidx
except (IOError, OSError):
# Probably situation is non-Linux, just do limited support for
# such platforms until other people come alonge
# such platforms until other people come along
for iface in netifaces.interfaces():
addrinfo = netifaces.ifaddresses(iface).get(socket.AF_INET6, [])
for addr in addrinfo:
v6addr = addr.get('addr', '').partition('%')[2]
if v6addr:
yield(int(v6addr))
break
return
@@ -88,10 +100,26 @@ 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 != 'sha512':
raise Exception("TODO: Non-sha512")
return 'sha512$' + hashlib.sha512(certificate).hexdigest()
if algo == 'sha256':
return 'sha256$' + hashlib.sha256(certificate).hexdigest()
elif algo == 'sha512':
return 'sha512$' + hashlib.sha512(certificate).hexdigest()
raise Exception('Unsupported fingerprint algorithm ' + algo)
def cert_matches(fingerprint, certificate):
@@ -99,8 +127,8 @@ def cert_matches(fingerprint, certificate):
return False
algo, _, fp = fingerprint.partition('$')
newfp = None
if algo == 'sha512':
newfp = get_fingerprint(certificate)
if algo in ('sha512', 'sha256'):
newfp = get_fingerprint(certificate, algo)
return newfp and fingerprint == newfp
@@ -111,11 +139,11 @@ class TLSCertVerifier(object):
self.fieldname = fieldname
def verify_cert(self, certificate):
fingerprint = get_fingerprint(certificate)
storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
)
if self.fieldname not in storedprint[self.node]: # no stored value, check
# policy for next action
if (self.fieldname not in storedprint[self.node] or
storedprint[self.node][self.fieldname]['value'] == ''):
# no stored value, check policy for next action
newpolicy = self.cfm.get_node_attributes(self.node,
('pubkeys.addpolicy',))
if ('pubkeys.addpolicy' in newpolicy[self.node] and
@@ -123,19 +151,46 @@ class TLSCertVerifier(object):
newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
# manual policy means always raise unless a match is set
# manually
fingerprint = get_fingerprint(certificate, 'sha256')
raise cexc.PubkeyInvalid('New certificate detected',
certificate, fingerprint,
self.fieldname, 'newkey')
# since the policy is not manual, go ahead and add new key
# after logging to audit log
fingerprint = get_fingerprint(certificate, 'sha256')
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'certautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {self.fieldname: fingerprint}})
return True
elif storedprint[self.node][self.fieldname]['value'] == fingerprint:
elif cert_matches(storedprint[self.node][self.fieldname]['value'],
certificate):
return True
fingerprint = get_fingerprint(certificate, 'sha256')
raise cexc.PubkeyInvalid(
'Mismatched certificate detected', certificate, fingerprint,
self.fieldname, 'mismatch')
numregex = re.compile('([0-9]+)')
def naturalize_string(key):
"""Analyzes string in a human way to enable natural sort
:param nodename: The node name to analyze
:returns: A structure that can be consumed by 'sorted'
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(numregex, key)]
def natural_sort(iterable):
"""Return a sort using natural sort if possible
:param iterable:
:return:
"""
try:
return sorted(iterable, key=naturalize_string)
except TypeError:
# The natural sort attempt failed, fallback to ascii sort
return sorted(iterable)
+6 -1
View File
@@ -12,7 +12,7 @@ Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Requires: python-pyghmi, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dns, python-netifaces, python2-pyasn1, python-pysnmp, python-pyte
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/
@@ -33,7 +33,12 @@ done
grep -v confluent/__init__.py INSTALLED_FILES.bare > INSTALLED_FILES
cat INSTALLED_FILES
%triggerin -- python-pyghmi
if [ -x /usr/bin/systemctl ]; then /usr/bin/systemctl try-restart confluent >& /dev/null; fi
true
%post
sysctl -p /usr/lib/sysctl.d/confluent.conf >& /dev/null
if [ -x /usr/bin/systemctl ]; then /usr/bin/systemctl try-restart confluent >& /dev/null; fi
true
+20 -1
View File
@@ -19,12 +19,28 @@
# Note that this script has a high chance of breaking confluent, so
# do not be surprised if confluent crashes as you exit...
import atexit
import os
import select
import socket
import readline
import sys
import threading
historypath = os.path.expanduser("~/.confluentdbghistory")
def save_history():
import readline
try:
readline.write_history_file(historypath)
except:
pass
if os.path.exists(historypath):
readline.set_history_length(1000)
readline.read_history_file(historypath)
atexit.register(save_history)
readline.parse_and_bind('tab: complete')
conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
conn.connect('/var/run/confluent/dbg.sock')
@@ -45,7 +61,10 @@ class GetInput(threading.Thread):
inputthread = GetInput()
inputthread.start()
while True:
r, _, _ = select.select((conn,), (), (), 0.1)
try:
r, _, _ = select.select((conn,), (), (), 0.1)
except select.error:
continue
if conn in r:
sys.stdout.write(conn.recv(1))
if pendingoutput is not None:

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