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

Compare commits

..

307 Commits

Author SHA1 Message Date
Jarrod Johnson da400b11d0 Temporarily back out check_power_state
It blocks ctrl-e mysteriously.
2017-06-14 09:21:08 -04:00
Jarrod Johnson d9042e5d4c Support newer pyte
pyte changed the buffer format, adjust strategy to be compatible both with new and old.
2017-06-13 09:06:43 -04:00
Jarrod Johnson e443aa6530 Fix line wrap in nodeattrib man page 2017-06-12 17:20:32 -04:00
Jarrod Johnson 8993f80b13 Update synopsis of the nodeattrib man page 2017-06-12 16:50:06 -04:00
Jarrod Johnson 8e00f3a795 Fix up nodeattrib usage message 2017-06-12 16:45:53 -04:00
Jarrod Johnson 1f6fa7636b Fix synopsis for nodeattrib -c 2017-06-12 16:10:02 -04:00
Jarrod Johnson d86bacf4d8 Add man pages for confluent and confluentdbutil 2017-06-12 14:58:34 -04:00
Jarrod Johnson 7230ec1306 Change nodelist usage message per test request 2017-06-12 14:40:04 -04:00
Jarrod Johnson 00d5416737 Merge branch 'discovery' of hpcgitlab.labs.lenovo.com:jjohnson2/confluent into discovery 2017-06-08 10:08:03 -04:00
Jarrod Johnson b22073e106 Limit children of noderun/nodeshell
Implement throttling of concurrent processes.  While 64 might be a bit conservative in this day and
age, we were exceeding FD_SETSIZE, and maybe we were also lucky.  Throttle to 168 for the moment.
2017-06-08 10:05:24 -04:00
Jarrod Johnson 03369c5d5e Fix handling of prefix length calculation
It was too naive about the mask, endiannes swaps bytes, not bits.
Fix by converting everytihng to big endian, where the mask is
guaranteed to be contiguous set of bits.
2017-06-05 09:21:58 -04:00
Jarrod Johnson 0b30b92bc9 Use default PIPE handling
Python overrides PIPE handling with silly behaviors, have the confluent commands restore
system defaults rather than Python behavior.
2017-06-01 09:03:15 -04:00
Jarrod Johnson dd90af1414 Revert "Implement Windows compatibility for slp, maybe ssdp"
This reverts commit 9e13724037.
2017-06-01 08:55:28 -04:00
Jarrod Johnson 9e13724037 Implement Windows compatibility for slp, maybe ssdp
Windows was unable to execute slp.  The use of inet_pton makes 'snoop'
impossible still, but perhaps not needed in a windows utility form...
2017-05-31 16:57:55 -04:00
Jarrod Johnson bbe839ea5d Fix noderun and nodeshell on long commands
Before it required 'rdy' to come back, and give up if empty.  Really we care about
all exclusively.
2017-05-31 16:26:55 -04:00
Jarrod Johnson ff7bdcc16e Avoid corrupting the terminal session on out of band issues
For out of band issues, keep the terminal state active.
2017-05-31 13:19:19 -04:00
Jarrod Johnson 6cd961786d Handle a blank result for bridge to ifindex
If blank, ignore and continue, rather than generating a trace.
2017-05-31 11:55:30 -04:00
Jarrod Johnson 18fa8f0b41 Do not generate trace on bridge without interface
Bridge ports can exist without a physical port.  Skip over such a port rather than error.
2017-05-31 11:52:46 -04:00
Jarrod Johnson 6e5375918d Fix unexpected error on node deletion
Node deletion was incorrectly trying to react to '_attrgrobs'.  Fix the
behavior to ignore it.
2017-05-31 08:53:54 -04:00
Jarrod Johnson 046c1e8ec4 Clean up periodic recheck scheduling
Observed that a number of concurrent rechecks were happening.  Do some clean
up in the hopes of straightening out the situation.
2017-05-30 14:06:31 -04:00
Jarrod Johnson eb48f2c1f7 Apply fingerprint match logic on recheck
On scan/snoop, we were ignoring if certificate already matched up.
Bring this logic to the recheck logic, so that things behave consistently.
2017-05-30 10:50:31 -04:00
Jarrod Johnson d25ac000f4 Have by-node get natural sorted 2017-05-30 09:18:11 -04:00
Jarrod Johnson a47bdf4181 Put discovery in order
Provide alphabetic ordering for most elements.  A TODO would be natural
sort at least for nodenames
2017-05-30 09:07:47 -04:00
Jarrod Johnson caf41345a1 Add 'by-state' to discovery
Provides a convenient method to focus on unidentified nodes.

Also, alphabatize the mac addresses on return.
2017-05-30 08:55:22 -04:00
Jarrod Johnson 3d6a1881c8 Fix [] inside regex
Regex uses [], support it in a normal way
2017-05-26 15:23:43 -04:00
Jarrod Johnson 5083977664 Rework node range evaluation priority
= and ~ operatiors would work incorrectly with '-', move filter and name regex up in priority
2017-05-26 15:06:52 -04:00
Jarrod Johnson 53821d98e6 Fix errors when switch and/or switchport are not set
It was nagging about conflicts, when not actually set.  Treat ''
as being the same as not setting at all.
2017-05-26 13:55:54 -04:00
Jarrod Johnson fdb8ef6919 Fix typoes in man page for nodeconsole 2017-05-26 13:20:34 -04:00
Jarrod Johnson b08bf6ae97 Be less aggressive about scanning interval
If there are slow switches in the mix, do our best to ensure solid
30 second interval of peace and quiet between scans, rather than a
scanning window per 30 seconds.
2017-05-26 12:52:42 -04:00
Jarrod Johnson 3318b3cda7 Ignore certificate check when manual
When doing manual discovery, repeat the process regardless of
certificate fingerprint state, since manual process is supposed
to do that.
2017-05-26 08:32:28 -04:00
Jarrod Johnson e97f768170 Fix format string of log message
The message was missing a close parenthesis.
2017-05-25 16:30:09 -04:00
Jarrod Johnson 9eacb4e3f0 Prioritize community if set
If the community is set, ignore user/password.  This fits expectations
a bit more.
2017-05-25 15:02:08 -04:00
Jarrod Johnson 77bce756cd Support secret.snmpcommunity
Also fix error where stale by-port data was left on a rescan.
2017-05-25 14:33:50 -04:00
Jarrod Johnson 2decaac8ea Be more tolerant on auto user deletion
Life is not perfect, we may not be able to really clean unexpected
accounts.
2017-05-23 16:14:32 -04:00
Jarrod Johnson e0a62a2533 Provide cleaner log when config fails
For now, provide a catch all to get some indication of problem,
rather than silent failure.
2017-05-23 10:53:54 -04:00
Jarrod Johnson f8d2fd8f46 Move up 'known_nodes' assignment
known_nodes is not some structure that was meant to be protected.
When an unexpected situation comes up (such as config failing), we
were losing track of the discovery info.
2017-05-23 10:44:15 -04:00
Jarrod Johnson 7f18f5fde0 Fix attrib based rediscovery
The wrong dictionary was offered up for manipulation.
2017-05-23 10:15:04 -04:00
Jarrod Johnson f07371c756 Add a python dbg cli, to use the dbg.sock without nc and with up arrow 2017-05-22 16:55:25 -04:00
Jarrod Johnson 179fcd9dcf Fix incorrect message in the login process failure message. 2017-05-22 14:28:52 -04:00
Jarrod Johnson e79a5ecd12 Merge branch 'discovery' of hpcgitlab.labs.lenovo.com:jjohnson2/confluent into discovery 2017-05-22 14:17:55 -04:00
Jarrod Johnson 2faf187d2d Use standard nodepawer usage on bad argument 2017-05-22 14:17:35 -04:00
Jarrod Johnson 703d8f273f Provide a script to summarize hangtraces files 2017-05-22 09:24:58 -04:00
Jarrod Johnson db0bc41441 Sort port entries in a show mac info
This means that the most interesting entry is probably the last.
2017-05-20 18:46:33 -04:00
Jarrod Johnson 97effaae01 Modify networking macs api to be more extendable and consistent
When we get down to a switch, make the macs 'by-mac', since 'by-vlan' is a likely future extension.
2017-05-20 18:07:35 -04:00
Jarrod Johnson 0a5424fbb2 Fix scope of module global dictionaries
In the refactoring, accidentally kept the map information
incorrectly scoped to be disposed of.
2017-05-20 17:41:59 -04:00
Jarrod Johnson 3571947332 Change names of portions of the API 2017-05-20 17:39:30 -04:00
Jarrod Johnson 72289f89cb Make sure switch update sweep completes in background
Switch scan was interrupted in the event of a successful scan.
Change to background execution on generatorexit.
2017-05-20 11:04:47 -04:00
Jarrod Johnson bf90431101 Implement no-qbridge mode and workaround for switch mistake
Sometimes switches erroneously use interface index instead of bridge index
in the bridge mib.  Workaround by assuming this is probably the case if
none of the macs seem to map to an interface.  Note that it's still important
to accept that a bridge port might not have an interface (internal mac address)
so only make the assumption if *none* of the mac addresses mapped to an interface.
2017-05-20 10:11:44 -04:00
Jarrod Johnson 784758fa25 Tweak documentation
For synopsis, remove optional indication.
For usage, change to common terminology with synopsis.
2017-05-17 15:19:22 -04:00
Jarrod Johnson 6c5536c840 Implement manual discovery
/discovery/by-mac/ -> set 40-f2-e9-af-45-a4 node=r1

Allow manual assignment as if it were detected through switch or chassis
2017-05-17 14:25:46 -04:00
Jarrod Johnson 64ebc9b16f Have by-type and by-model filter each other
There was too much misleading output when combining them.
2017-05-17 11:49:50 -04:00
Jarrod Johnson 213b027f62 Report unavaliable sensors as unavailable rather than timeout 2017-05-17 11:16:27 -04:00
Jarrod Johnson e9ddbb8785 Rework discovery api
The code is much cleaner, much less repitition.
2017-05-16 17:18:23 -04:00
Jarrod Johnson 28eb1180c7 Only join the thread if it is there 2017-05-15 11:36:50 -04:00
Jarrod Johnson 892d01d054 Fix use of WindowsError
OSError is the base class, use that instead of WindowsError
since Linux python is befuddled.
2017-05-15 11:34:11 -04:00
Jarrod Johnson 949609ce64 Be picky about the input message
set rescan=start

Is the valid argument
2017-05-15 11:02:53 -04:00
Jarrod Johnson 21b99728d7 Be picky about where rescan is called, just in case 2017-05-15 10:59:41 -04:00
Jarrod Johnson 79b4482298 Provide rescan functionality for network macmap 2017-05-15 10:57:24 -04:00
Jarrod Johnson 40f700c46d Provide SLP rescan
Create /discovery/rescan resource, and hook in an SLP rescan.
2017-05-15 10:16:54 -04:00
Jarrod Johnson 652d9c0a55 Support PXE based discovery
PXE can't do https, so core needed a fair amount of work
to deal with handler without https (or any ipaddr for that matter).
2017-05-12 16:46:45 -04:00
Jarrod Johnson 20d7eb62f4 Support gateway setting in bmc discovery 2017-05-12 09:55:10 -04:00
Jarrod Johnson 34623d5bbd Document plans for medium term PXE discovery
Handling of getting the mac address into the attributes accurately
is complicated, document the design point for future.
2017-05-12 08:32:57 -04:00
Jarrod Johnson 0e7072a256 Fix support for ports with hyphens in the name 2017-05-11 16:48:54 -04:00
Jarrod Johnson 298b9bcfff Add net.ipv4_gateway attribute and function to fetch config
Provide a command to implement common extraction of nic configuration
from the attributes, so that code can be extended/manipulated in a
common place.
2017-05-11 16:40:59 -04:00
Jarrod Johnson ddfdae6bf0 Change to using KeyValueData message for discovery and mac data
This is properly representing the data as read-only information.
2017-05-10 17:58:23 -04:00
Jarrod Johnson b77a97db43 Add '/networking/' section to the API
Provide a mechanism to explore the internal information about switch
mac tables.
2017-05-10 14:28:57 -04:00
Jarrod Johnson 1e1d8dc000 Support trying default then target credentials
If something is discovered that has already been set up, try
first the default credentials, and failing that see if perhaps
the final user/password will work.  If the credentials match neither,
then there's nothing for us to do than log.
2017-05-09 14:40:27 -04:00
Jarrod Johnson 4adb2adc12 Merge branch 'master' into discovery 2017-05-09 08:39:17 -04:00
Jarrod Johnson 0e9e5da102 Properly respond with not found under by-type and by-model
Previously, a KeyError would trigger Unexpected error, correct this.
2017-05-08 16:53:55 -04:00
Jarrod Johnson 777c419cb8 Backout checking for str/unicode
A dict is a perfectly cromulant value for an attribute (e.g. with an
expression).
2017-05-08 16:50:55 -04:00
Jarrod Johnson 96916b57ae Support net.* for multiple nics in macmap 2017-05-08 14:27:09 -04:00
Jarrod Johnson 40ccf9d325 Support fetch by wildcard
Code needs to be able to retrieve by wildcard to be effective,
provide that support again limited to fnmatch '*' style wildcards.
2017-05-08 14:15:56 -04:00
Jarrod Johnson 6bd36580ba Support net.* attributes
Allow named groupings of net attributes.
2017-05-08 13:53:38 -04:00
Jarrod Johnson 37f12296b8 Support wildcard watchers
In preperation for 'net.<name>.attributes', provide
ability for code to watch wildcards to get the open ended ones.
2017-05-08 12:00:16 -04:00
Jarrod Johnson cd9eefa100 Document plan change on nic attributes
Document change is long term strategy on the network attributes.
The previous design was an overreaction to the mess of the
nics/mac/hosts table and inability to cleanly deal with bonds and
bridges.  I think this design does a better job compromising between
overcomplicating the simple case and having the power to accomodate
the complex case.  Note that IMO, network manager overcomplicates the
device to connection relationship, and I want to avoid that.
2017-05-08 11:07:46 -04:00
Jarrod Johnson 2c72931641 Add status to nodepower man page
Would normally have omitted it, but it is in usage.  And the usage
message is cleaner without having to explain you can skip status.
Have the man page explain that 'status' acts the same as no argument.
2017-05-08 10:39:53 -04:00
Jarrod Johnson 0731bfe29a Add bulk node creation and deletion
Now create and rm can be done in /noderange (user beware)
2017-05-05 16:55:40 -04:00
Jarrod Johnson 667a1030b4 Fix node*attrib commands show of undefined attributes
It would unfairly call things an invalid attribute.
2017-05-05 15:30:10 -04:00
Jarrod Johnson 4c6e3fcc4f Change powered off notification 2017-05-05 15:16:08 -04:00
Jarrod Johnson baa4d8bd1a Support 'custom.' prefix attributes
Allow freeform use of 'custom.' prefix, for arbitrary
attributes of user chosing (without type and typo checking)
2017-05-05 15:03:31 -04:00
Jarrod Johnson 816445e4b6 Fix man page directory location 2017-05-03 16:28:21 -04:00
Jarrod Johnson 8cce35dbcb Correct typo in nodeattribexpressions man 2017-05-03 15:01:13 -04:00
Jarrod Johnson dabb93fc04 Fix man page paths for sections 5 and 8 2017-05-03 14:33:17 -04:00
Jarrod Johnson 120b70f7ab Add usage to nodegroupattrib now that it is repaired 2017-05-03 14:30:04 -04:00
Jarrod Johnson fb626f7713 Fix nodegroupattrib command 2017-05-03 14:27:16 -04:00
Jarrod Johnson 41c22d9e66 Provide more man pages
Correct the lack of documentation for confluent commands.
2017-05-03 14:27:05 -04:00
Jarrod Johnson 6b6b7f06b7 Merge branch 'master' into discovery 2017-05-03 09:45:39 -04:00
Jarrod Johnson 4595c417ee Merge branch 'master' into discovery 2017-05-02 15:39:33 -04:00
Jarrod Johnson f5fea79ee4 Provide info.note attribute
Provide an attribute for open ended human use.
2017-05-02 14:26:28 -04:00
Jarrod Johnson 58ab997105 Fix nodeattrib behavior on undefined attribute. 2017-05-02 09:58:14 -04:00
Jarrod Johnson 819318b763 Add man page for nodeidentify 2017-05-01 16:14:21 -04:00
Jarrod Johnson d47dfd8a73 Add a man page for nodepower 2017-05-01 16:07:12 -04:00
Jarrod Johnson bcfc6966c3 Update the man page for nodeconsole 2017-05-01 14:19:08 -04:00
Jarrod Johnson e1cb97e836 Fix formatting issues in nodeattrib man 2017-05-01 14:11:44 -04:00
Jarrod Johnson 7f2df84167 Fix nodeattrib behavior on undefined attribute. 2017-05-01 14:04:26 -04:00
Jarrod Johnson ce1a5b464f Fix nodeattrib behavior without argument 2017-05-01 14:02:41 -04:00
Jarrod Johnson 4fb41a991e Fix formatting of nodeattrib man page 2017-05-01 13:45:29 -04:00
Jarrod Johnson bd735e8902 Fix the nodeinventory args parsing without 'firm' 2017-04-28 16:02:32 -04:00
Jarrod Johnson 7dcbd75561 Add full path to man pages 2017-04-28 15:35:16 -04:00
Jarrod Johnson 4ec06f8940 Fix argument to ronn to generate roff 2017-04-28 15:12:17 -04:00
Jarrod Johnson 60b91d5202 Invoke man page builder for rpm build 2017-04-28 15:10:09 -04:00
Jarrod Johnson 4ce382ced2 Add a script to build man pages 2017-04-28 15:09:00 -04:00
Jarrod Johnson d85fddd555 Prepare for man page packaging 2017-04-28 15:07:06 -04:00
Jarrod Johnson c150202585 Correct argv for nodefirmware from nodeinventory 2017-04-28 14:27:57 -04:00
Jarrod Johnson 8cde65a9e6 Correct the exec in previous commit 2017-04-28 13:59:48 -04:00
Jarrod Johnson d1abace7da If 'firm' appears as the argument, make it nodefirmware
This better enables use of alias rinv to nodeinventory.  Though it
is a smidge slower than nodefirmware.
2017-04-28 13:52:13 -04:00
Jarrod Johnson 412166922a Add '/version'
Provide version info via API
2017-04-28 13:20:26 -04:00
Jarrod Johnson 5e34ebe525 Quote the version number. 2017-04-28 11:54:41 -04:00
Jarrod Johnson 8274167298 Add version info to confluent module 2017-04-28 11:39:57 -04:00
Jarrod Johnson f5e6461fd2 Add help to confetty
Help command and text, generic for now.
2017-04-28 10:50:59 -04:00
Jarrod Johnson c9ffe77582 Make ls/dir behavior consistent with filesystem
In a filesystem, 'ls' does not become 'cat'.  Have confetty do the
same thing.  'cat' and 'show' act the same.
2017-04-28 10:15:15 -04:00
Jarrod Johnson 60ec9c7c51 Background nodelist change handling
The discovery reaction to a node being deleted or added is laborious, and redundant.  Background
the handling and make sure only one instance executes and aggregate redundant rechecks to a single
re-execution at the end.
2017-04-28 09:14:55 -04:00
Jarrod Johnson 1d04405099 Merge branch 'master' into discovery 2017-04-27 16:49:26 -04:00
Jarrod Johnson 16db3f1b6a Merge branch 'master' into discovery 2017-04-27 16:48:20 -04:00
Jarrod Johnson a1b0ab15c5 Change to str for getting string of shlex
ve.message was deprecated, but str is current.
2017-04-27 16:01:30 -04:00
Jarrod Johnson 2a1794ddad Enrich snoop SLP to look like scan
Scanned data was doing work to flesh out responses, but not snoop.

Add attributes to snoop flow and make the result data look more
similar.
2017-04-27 16:01:06 -04:00
Jarrod Johnson 7704523410 Provide discovery detail in deep levels 2017-04-27 14:48:59 -04:00
Jarrod Johnson 4cc57acea9 Provide detail information on detected nodes
More data may come, but for now provide the basic
information like serial number, model number, detected nodename,
ip address, and so forth.
2017-04-27 14:18:22 -04:00
Jarrod Johnson 70aa1fd114 Merge branch 'master' into discovery
# Conflicts:
#	confluent_server/confluent/plugins/hardwaremanagement/ipmi.py
2017-04-26 14:38:05 -04:00
Jarrod Johnson ba27207b83 Rework ipmi worker management
Provide a more concrete measurement of
children, rather than relying upon a
sentinel value on the queue.  It seems
that even using 'finally' didn't assure
that we always get that sentinel value
before a worker dies.  Sentinel value
still used to avoid a long wait in the
usual case.
2017-04-25 15:26:56 -04:00
Jarrod Johnson be12f163a5 Stub ability to get detailed info in discovery
Looking directly at a chunk of information,
temporarily use repr as a stub until a proper messages
is done.  This would break horribly in the web, for example.
2017-04-24 15:48:44 -04:00
Jarrod Johnson 20e00b266a Merge branch 'master' into discovery 2017-04-21 10:30:02 -04:00
Jarrod Johnson ae03ca9fd8 Merge remote-tracking branch 'upstream/master' into discovery 2017-04-20 16:26:05 -04:00
Jarrod Johnson 1812e16559 Merge branch 'master' into discovery 2017-04-20 14:59:52 -04:00
Jarrod Johnson be971fd5e7 Comment out not yet implemented autonode attributes
Since this is going to be pulled out of scope for now,
comment out the attributes until relevant.
2017-04-17 16:47:05 -04:00
Jarrod Johnson 8675929a16 Coerce binary operation arguments to numerics
This supports numeric manipulation of other attributes.
2017-04-12 13:26:48 -04:00
Jarrod Johnson a2d440d83b Correct missing argument
__new__ was missed in the last commit.
2017-04-11 15:41:26 -04:00
Jarrod Johnson e0d6d6b18c Revert "Try to mitigate chance of two greenthreads writing"
This reverts commit d83fb76254.

This could get stuck in various scenarios.
2017-04-11 15:16:04 -04:00
Jarrod Johnson e47038b2dd Change the stdout/stderr/trace logs to not buffer things over time
If something went completely off the rails, it could easily fill up lots of memory with log entries in the 2 seconds it
would buffer.  For now disable the buffering on key debug logs, as the main purpose was reducing IOPs in the per-node
console logs anyway.  A future behavior may be to also limit the size and/or number of outstanding log entries before
committing to disk.
2017-04-11 14:39:29 -04:00
Jarrod Johnson a5e84e2eb0 Rework some of the explorable discovery data
This is a bit easier to read and avoids some code replication.
2017-04-07 16:52:18 -04:00
Jarrod Johnson 6f633e0bfb Address a number of corner cases in console
The pyte parser state can get corrupted.  Unfortunately, we
don't know how, but we can detect and repair.

Also, the number of livesessions can change mid iteraction, change code to deal with changes
in the midst of handling data.

Finally, if trying to write data to the console fails for any unexpected reason,
log the trace and start the disconnect process.
2017-04-07 13:44:40 -04:00
Jarrod Johnson 2112fd71f5 Merge branch 'master' into discovery 2017-04-07 09:41:32 -04:00
Jarrod Johnson d83fb76254 Try to mitigate chance of two greenthreads writing
consoleserver, at least for one, is writing data direct.
At least once in a while, from a long distance from a potentially
spotty network connection is triggering a conflicting write.
Hopefully this mitigates it.
2017-04-06 11:25:33 -04:00
Jarrod Johnson 064514d6ec Fix omitted dl
dl parameter was omitted by mistake in previous commits.
2017-04-04 16:22:39 -04:00
Jarrod Johnson 3e8271afa8 Revert "Declare stdout to be in utf-8 across the board"
This reverts commit 26a8ba53bd.
2017-04-04 14:52:45 -04:00
Jarrod Johnson 3980be4e3a Revert "Use raw stdout for console"
This reverts commit 3b8d0ef40a.
2017-04-04 14:52:24 -04:00
Jarrod Johnson 67d61fc803 Revert "Use native stdout in confetty"
This reverts commit 439f4c7709.
2017-04-04 14:52:10 -04:00
Jarrod Johnson 5f3f073e9c Fix tab completion on things with hyphens in it. 2017-04-04 11:58:55 -04:00
Jarrod Johnson 439f4c7709 Use native stdout in confetty
A number of things break other than console, like tab completion.
2017-04-04 11:29:32 -04:00
Jarrod Johnson 3b8d0ef40a Use raw stdout for console
The codecs streamwriter does poorly with some console
data.
2017-04-04 11:11:10 -04:00
Jarrod Johnson 26a8ba53bd Declare stdout to be in utf-8 across the board
Python changes behavior when going through a pipe, consistently use UTF-8
instead.
2017-04-04 09:00:21 -04:00
Jarrod Johnson c9192ab170 Do not take invalid serial numbers
Serial numbers that are empty or all spaces are now filtered.
2017-04-04 08:55:39 -04:00
Jarrod Johnson 00598578a6 Numerous discovery anomaly fixes
For one, ensure mac address is valid in the neighutil.py, rather than
trying to blacklist.  Apart from FAILED, INCOMPLETE is possible,
and it's just a bad idea to blacklist.

When getting the https_cert has an unexpected situation, now
say it is because of unreachable all the time.

Refactor the loop in the node recheck so that the inner loop is brought out.
Catch and print any exceptions, but carry on to the next entry regardless.

Do not exit out of above loop because one thing is unreachable.
2017-04-03 11:33:52 -04:00
Jarrod Johnson 74740e01b1 Further expand the discovery api support 2017-03-31 16:59:19 -04:00
Jarrod Johnson 0034309fe1 Flesh out some of the api 2017-03-31 16:49:02 -04:00
Jarrod Johnson 0b7f2a45e9 First phase of API explorable discovery 2017-03-31 16:06:33 -04:00
Jarrod Johnson 261783f971 Provide instrumentation functions
Prepare to enable API browsing of discovery data
2017-03-31 15:58:08 -04:00
Jarrod Johnson 9f8feb971d Remove the unused skeleton of discovery
It was useful as note, but the actual design deviated.
2017-03-31 14:37:45 -04:00
Jarrod Johnson 2959fa7e33 Add IMM discovery
Apart from CMM, IMMv2 platform systems should work in theory.
2017-03-31 11:33:09 -04:00
Jarrod Johnson 0b40b876c0 Provide more specific guidance on SNMP issues
Previously, many things manifested as 'Timeout',
particularly bad credentials.  Provide some fixes
to the log messages.
2017-03-30 14:56:21 -04:00
Jarrod Johnson 6952f7cab9 Fix discovery and messaging around certain scenarios
If a node has a random ip address, then static, then goes back,
it was not updating the addresses.  When an address is updated,
go ahead and do a deeper investigation.

If anything should go wrong unexpectedly, don't let the periodic recheck
forever die.

If for some reason we have bad ip in the info dictionary, log a more accurate
message.
2017-03-30 12:22:53 -04:00
Jarrod Johnson c2c81b0167 Do not use a FAILED entry for arp
An arp entry for FAILED is not usable, so do not
accept that either.
2017-03-30 11:04:35 -04:00
Jarrod Johnson ac0626a51c Provide more information about unexpected error
We know the specific ip, mac, and *maybe* nodename,
provide that info to help debug an issue.
2017-03-30 09:44:47 -04:00
Jarrod Johnson 9effdee15a Guad recheck with a semaphore
While measures have been taken to improve sanity,
should something go wrong, make recheckers that are duplicate
run in serialy rather than parallel.
2017-03-29 15:55:48 -04:00
Jarrod Johnson 65580b1d60 Avoid exponential growth of recheckers
cancel() happening while _periodic_recheck is already running
would result in duplicate periodic_rechecks, each which could
then repeat the mistake, leading to exponential growth of
the recheck process.
2017-03-29 15:53:10 -04:00
Jarrod Johnson 9c41031df3 Fix mac scanning back to back
If find_node_by_mac were called in different contexts concurrently, the
slightly later starter would see the vintage be recent and bail, though
scanning is not, in fact ready yet.  Change to early exit strategy after checking
the semaphore phase, to make concurrent calls wait.
2017-03-29 14:45:13 -04:00
Jarrod Johnson 740cccd82e Tolerate a target that won't TCP SLP 2017-03-29 14:33:30 -04:00
Jarrod Johnson 344bb1229c Wrap mac map update in a semaphore
We only want one scan of the switches occurring at any given moment.
2017-03-29 09:48:44 -04:00
Jarrod Johnson 935acc2a10 Avoid futile thrashing on switch search 2017-03-29 09:28:25 -04:00
Jarrod Johnson cd841fb43e Update comments, add a rescan for future 2017-03-27 13:52:04 -04:00
Jarrod Johnson 447a3b3ffe Accept updated ip addresses
Merge new ip info rather than just discarding it.
2017-03-27 11:11:18 -04:00
Jarrod Johnson 108c9d3e9d Do not actually discover if config errors are found
We will defer until someone actually fixes them.
2017-03-24 15:31:11 -04:00
Jarrod Johnson cc98290d68 Fix a number of issues when switches misconfigured
For one, if a "" is used, ignore that as a switch.
Second, if "" is a username, then treat that as 'None'.
Finally, make sure event shows something even if it doesn't know what it is.
2017-03-24 15:21:36 -04:00
Jarrod Johnson 1fce78ed39 Adjust for SMM fix
We can now more quickly set the IP address, as the SMM problem is really
resolved.
2017-03-24 10:47:25 -04:00
Jarrod Johnson 5bc4d20eef Accelerate recheck on apparent https not running
If we see https is not running, be aggressive and check back in
a minute, rather than waiting 15 minutes, to reduce discovery latency.
2017-03-21 16:10:40 -04:00
Jarrod Johnson 5dbd345a49 Avoid scriptlet error if it's no big deal 2017-03-21 15:46:03 -04:00
Jarrod Johnson 62839b76b7 Move probe into per node function
probe() was being missed on updates.  Now it hits both.

Also, log and defer if node is not running https cert on recheck.
2017-03-21 14:49:35 -04:00
Jarrod Johnson 848745bb4a Store list for future use 2017-03-21 11:21:24 -04:00
Jarrod Johnson f68a8c80bb Implement alternate enclosure scheme
Previously, it relied upon the handler successfully
populating 'enclosure.bay'.  It seems that failures can happen
after all, so instead check if it *should* be an enclosure, and
use a more bulletproof indicator of whether the thing *is*
an enclosure.
2017-03-21 10:37:52 -04:00
Jarrod Johnson 7f185db64f Track known_nodes and unknown better
If something was known from a prior life, still store it as
a known_nodes to allow config changes to be handled.
Also, if an unexpected condition occurs, ensure that
we track it as unknown.
2017-03-21 10:17:36 -04:00
Jarrod Johnson 2cd2e170e0 Merge branch 'discovery' of hpcgitlab.labs.lenovo.com:jjohnson2/confluent into discovery 2017-03-21 08:27:27 -04:00
Jarrod Johnson 0df75d3102 Stop trying to logout on preconfig
The logout was stepping on itself.
2017-03-20 18:50:38 -04:00
Jarrod Johnson 5b8aebaa3e Remove known nodes from unknown_info
Regardless of outcome, a discover_node should
not be in 'unknown_info' anymore.
2017-03-20 11:12:44 -04:00
Jarrod Johnson 0577c9a99c Suppress error output on try-restart 2017-03-18 17:32:48 -04:00
Jarrod Johnson 12ffdf9504 Clear titlebar on exit
If the system running confetty does not manage titlebar, clear what we have
on way out, to avoid misleading output.
2017-03-18 16:07:25 -04:00
Jarrod Johnson c2a4913c78 Defer discovery of targets not running https
The https certificate is critical for ascertaining whether something is new or
existing.  Also, config step may depend upon https availability if we have
pre-approved https certificates.
2017-03-18 15:03:13 -04:00
Jarrod Johnson f34770529e Fix detection of well known nodes up front
The second parameter was not being coerced into an iterable.
2017-03-18 14:39:24 -04:00
Jarrod Johnson 0336a07f17 Only log on real discovery
Was logging on storing already discovered nodes, fix the mistake.
2017-03-18 14:24:30 -04:00
Jarrod Johnson b57dbcd89b Fix spelling of permissive 2017-03-18 14:03:01 -04:00
Jarrod Johnson 7948cd1bb2 Fix attriute responsiveness
The recheck procedure can delete parts of unknown_info.  Detect and
move past.
2017-03-18 13:59:14 -04:00
Jarrod Johnson 049a728be1 Improve appearance of no buffer message 2017-03-18 13:34:15 -04:00
Jarrod Johnson 42c775a7a4 Remove attempt to rebuild buffer from log file
It's at odds with the strategy of valid buffer only if we have confidence.
Clients reconnecting without replay still have a good experience, since their
terminal serves as the buffer.
2017-03-18 12:20:32 -04:00
Jarrod Johnson 9cf074e1a2 Log some discovery behavior
Provide some guidance on the flow of discovery and suggestions
if the user is facing a surprise situation.
2017-03-18 12:13:57 -04:00
Jarrod Johnson 631509a32a Enhance buffer replay behavior
Provide a clearer message explaining why no replay buffer.  Also clear the message away
when data is sent/captured.
2017-03-18 11:09:41 -04:00
Jarrod Johnson 4e4fbeb907 Go back to resetting the SMM on config
The SMM issue around config is not fixed after all.
2017-03-17 10:08:43 -04:00
Jarrod Johnson b92fddc647 Clear buffer on loss of connection
When a connection is lost, the data is at high risk of being stale.  In such
a case, clear out the current terminal buffer with a message.
2017-03-16 16:54:03 -04:00
Jarrod Johnson 79474ea9fb Restore cursor position explicitly
This allows full redraw of whitespace and very precisely
restores the position of the cursor to the right place.
2017-03-16 11:27:30 -04:00
Jarrod Johnson 639a7352ad Change to stateful utf8 decoding
Since we are receiving bytes of data, switch to incremental decoder, which
will accomodate partial multi-byte utf-8.  Also simplify by using translate and
skipping the final utf-8 decode attempt, which would be impossible, but use
'replace' in cp437, just in case.
2017-03-16 11:13:38 -04:00
Jarrod Johnson 4708630bac Put cursor in correct place on most reconnects
Before, the terminal redraw would draw to the end.  Avoid
trailing new line and whitespace to make prompt look almost right.
Space in PS1 is not honored.
2017-03-15 15:12:33 -04:00
Jarrod Johnson a0be58c9cb Provide cleaner error on a missing plugin
If a missing or mispelled plugin were specified, it would take out a whole
noderange.  This will provide a useful message and isolate the fault.
2017-03-15 14:51:19 -04:00
Jarrod Johnson 71e89c005b Let slip the dogs of discovery
We have firmware fixes in place, we can enable
pretty much the full discovery.
2017-03-15 11:25:53 -04:00
Jarrod Johnson 1dc8fc973d Rework the normalize
Seemingly, decode then encode can mess up some utf-8.
If possible, leave it untouched, apart from binary replacement.
translate won't work on a byte string, so use replace.
2017-03-15 10:07:18 -04:00
Jarrod Johnson a517436f71 On IO error, drop failed input
It is possible to get an IO error on input.  Ignore this and carry on.
2017-03-15 09:29:06 -04:00
Jarrod Johnson ed34b30992 Implement a normalization to UTF-8
Not only using the codec, but also giving it a bit of 'help' on
CP437 characters not caught by encode.
2017-03-14 22:37:33 -04:00
Jarrod Johnson 8ca1553c33 Switch to a skip above maxlen
Previous strategy would not account for data vs. control codes.
2017-03-14 18:38:33 -04:00
Jarrod Johnson 02266a4be6 Allow whitespace padding to max detected width
This makes some background drawing look nicer.
2017-03-14 17:35:51 -04:00
Jarrod Johnson 70e5d1dc52 Note to implement better whitespace 2017-03-14 17:23:45 -04:00
Jarrod Johnson 191726776c Delete now stale code
It was preserved 'just in case', but new code is working and we always have git.
2017-03-14 16:53:05 -04:00
Jarrod Johnson c99d446a63 Add missing pyte dependency 2017-03-14 16:42:44 -04:00
Jarrod Johnson e3318ec5ef Implement virtual terminal
Provide a virtual 100x31 terminal instead of a dumb byte buffer.
This allows higher quality redraw on reconnect.
2017-03-14 16:21:26 -04:00
Jarrod Johnson cbded58fc2 Retry failed write with partial writes
Sometimes when dealing with certain terminal settings
and large volume of data, we need to fall back to a slow
scheme to write out.
2017-03-13 13:57:37 -04:00
Jarrod Johnson 005877d3eb Rename hardwaremanagement.switch and switchport
Make it net.switchport and net.switch to avoid inconsistency
when ultimately implementing support for in-band and out of
band nics
2017-03-07 14:11:44 -05:00
Jarrod Johnson 8497c28ebe Defer node add watcher as well
For now, keep delete watchers as is,
so that handlers have a chance to say goodbye
before the nodes actually go away.
2017-03-06 08:52:28 -05:00
Jarrod Johnson 2d76c0aefc Log a couple of common cases that don't need to be trace
Start using the event log to record things like this.
2017-03-05 10:04:46 -05:00
Jarrod Johnson 2e5ec9264f Pass an empty tuple to recheck_nodes
recheck_nodes is expecting an iterable, so give it one.
2017-03-05 09:47:25 -05:00
Jarrod Johnson 9734aa65bf Merge branch 'master' into discovery 2017-03-05 09:21:38 -05:00
Jarrod Johnson 46ed0eb38a Merge branch 'master' into discovery 2017-03-05 08:29:59 -05:00
Jarrod Johnson 85d2a0e6aa Fix disambiguation 2017-03-03 19:06:15 -05:00
Jarrod Johnson 5f88067134 Merge branch 'master' into discovery 2017-03-03 18:41:26 -05:00
Jarrod Johnson eb63a6fcda Note to show strategy for disambiguating enclosure/node
It is a tricky situation when detecting on switch.  If something has a bay
and is in fact an enclosure.manager itself, that means it will be a node
inside the enclosure, and not the enclosure manager.
2017-03-03 17:11:31 -05:00
Jarrod Johnson 8be3c2a80e Add some support for resetting discovered nodes
If a node has been discovered incorrectly, react to attempts to clear the
info to reconfigure the node.
2017-03-03 16:56:25 -05:00
Jarrod Johnson 2ce26cbb59 Translate name to IPv4 address 2017-03-02 17:02:17 -05:00
Jarrod Johnson 44808fd343 Translate name to IPv4 address 2017-03-02 16:14:54 -05:00
Jarrod Johnson 467fea7d8e Accept resolvable names for prefix length 2017-03-02 15:43:57 -05:00
Jarrod Johnson 39463ddcb7 Add missing rpm dependencies 2017-03-02 14:27:45 -05:00
Jarrod Johnson cdb3801993 Fix notifier handler involvement in setting value
Avoid extraneous error in setting values and performance
issue when manipulating an attribute with a registered watcher.
2017-03-02 11:20:17 -05:00
Jarrod Johnson af478816e9 Add hardwaremanagement.manager to monitor
React to a change in the hardwaremanagement.manager value,
to allow for cases where a user sets it after it would have been
needed.
2017-03-01 15:33:53 -05:00
Jarrod Johnson 79e57a442d Actually run the discovery mechanism 2017-03-01 15:06:25 -05:00
Jarrod Johnson 38d6354725 Provide mechanism to recheck nodes
When configuration changes happen, double check known(ish)
targets to see if they can discover now.
2017-03-01 14:58:25 -05:00
Jarrod Johnson b2ba28f0fb Implement workaround for SMM config
SMM configuration only reliably applies after reset.
Reset SMM to enact process for now.
2017-03-01 11:45:19 -05:00
Jarrod Johnson 9bf8d65df1 Merge branch 'master' into discovery 2017-03-01 10:31:49 -05:00
Jarrod Johnson 7c048c9c54 Merge branch 'master' into discovery 2017-03-01 10:24:42 -05:00
Jarrod Johnson 9fbf500b6c Make configuration of detected things be concurrent
This should speed up discovery in event of a slow target.
2017-03-01 09:38:11 -05:00
Jarrod Johnson 147be0c92c Handle XCCs without some attributes 2017-02-28 16:03:56 -05:00
Jarrod Johnson 54583c604f Do not encounter fatal issue when https is not running 2017-02-28 15:15:19 -05:00
Jarrod Johnson de41a665b2 Merge branch 'master' into discovery 2017-02-28 14:08:53 -05:00
Jarrod Johnson 98981f00a9 Actually set up Nodes managed by SMM
The XCCs are now configured if the policies allow it, whether or not the SMM is configured.
2017-02-27 17:04:52 -05:00
Jarrod Johnson 074a8debf4 Setup user on generic bmc case 2017-02-24 15:38:27 -05:00
Jarrod Johnson a4d66cdbc9 Protect the protocols from exceptions
Consolidate unexpected error handling in a safe_detected flow.
2017-02-23 14:53:12 -05:00
Jarrod Johnson 2270e64148 Have PXE return services in a consistent fashion 2017-02-23 14:48:15 -05:00
Jarrod Johnson 0e99d99051 On an XCC, probe to get uuid and enclosure info as possible. 2017-02-23 14:45:38 -05:00
Jarrod Johnson b0ddc66e06 Move SMM notes to smm.py for future
Don't clutter the bmc.py with the SMM notes
2017-02-22 16:23:01 -05:00
Jarrod Johnson 364a17f31a Implement IPv4 addr/netmask config apply
Also start getting ready for user/password code tomorrow.
2017-02-21 16:58:07 -05:00
Jarrod Johnson 04d18c4b22 Complete module rename in xcc 2017-02-21 14:19:17 -05:00
Jarrod Johnson 44330f2351 Add new subdirectory to setup.py 2017-02-21 14:13:14 -05:00
Jarrod Johnson 117cf71065 Rearrange the protocols into a subdirectory
It's a little less cluttered this way.
2017-02-21 13:38:24 -05:00
Jarrod Johnson 44b58d58ae Fix message on BMC user creation 2017-02-21 11:02:20 -05:00
Jarrod Johnson b5a924bdb6 Handle null fingerprint
If no existing fingerprint, then a cert can't possibly match, return
False.
2017-02-21 09:37:31 -05:00
Jarrod Johnson d1fd17200f Fix spelling mistake in cert_matches 2017-02-21 09:29:46 -05:00
Jarrod Johnson b035bd6c1e Add hooks to recheck on reconfigure
This will allow us to be responsive to changes in configuration.
2017-02-21 09:17:57 -05:00
Jarrod Johnson 02e2cb1673 Further advance discovery
This commit starts to lay the groundwork for
handling the security policy differences with
respect to TLS keys on port 443.  This requires
that the discovered entitity support https for now,
which is a given (even if the web service is not enabled,
it does provide a cert).

IMMv2 may never be able to follow the 'secure' path due
to uncertainty of whether there's a licensed web ui or not).
2017-02-18 09:23:34 -05:00
Jarrod Johnson 53954426a1 Begin working on xcc interrogation/preconfig 2017-02-17 17:01:11 -05:00
Jarrod Johnson d02c4e51f9 Fix mistakes in the discovery code to date 2017-02-15 17:11:59 -05:00
Jarrod Johnson 26e9db263d Add discovery and networking bits to packaging 2017-02-15 16:53:42 -05:00
Jarrod Johnson ae56085f95 Fix typo in the command 2017-02-15 15:36:23 -05:00
Jarrod Johnson 125160e0f5 Add attribute to control discovery policy 2017-02-15 15:23:26 -05:00
Jarrod Johnson 2f251dd05a Continue work on mac search capability 2017-02-15 14:03:28 -05:00
Jarrod Johnson 88b2b49d3b Merge branch 'master' into discovery 2017-02-15 11:53:12 -05:00
Jarrod Johnson 1fb5c0c177 Begin work on node handlers
The handler framework will be the key for supporting multiple node types.
Each handler is supposed to support a variety of stages through the
process.

First a unicast interrogation to get some data (ideally without
logging in, or by logging in with default credentials).

Second upon identification, preconfig will be called, if and only if the endpoint would become a node,
but should not yet do anything big, just paving the way for verification or dependent discovery configuration.

Finally, config will be called to deploy configuration according to the node and referenced
config manager.
2017-02-15 11:47:05 -05:00
Jarrod Johnson 9fdccf8371 Begin working on xcc interrogation/preconfig 2017-02-14 17:07:49 -05:00
Jarrod Johnson f5d7fdcb58 Update design commentary in discovery
Note the preconfigure stage
2017-02-14 16:34:47 -05:00
Jarrod Johnson b639720190 Tweak available attributes. 2017-02-14 16:34:19 -05:00
Jarrod Johnson 347dbd8cd1 Move neighutil up to a more general location
It isn't necessarily discovery specific
2017-02-14 16:26:45 -05:00
Jarrod Johnson 204b47d174 Add function to get the prefix length
When we get to configuration of a system,
we currently will have the ip address, but
not the prefix length.  Use this as a way
to divine the information automatically.
2017-02-14 16:24:21 -05:00
Jarrod Johnson d738a4c61f Do not repr twice
Data in the initial scan stage was repeated twice in
unit test.  Fix the extra output
2017-02-13 17:00:27 -05:00
Jarrod Johnson 6ebbd91592 Fix handling of certain scenarios
One issue is that SLP data can have bad data in the bytes. Present
a decode error.  So far, the raw value was erroneously '\xff\xff....'

Another is that a network with aliases will reject IP_ADD_MEMBERSHIP, due
to the ambiguity.
2017-02-13 16:52:44 -05:00
Jarrod Johnson 17f587252e Merge branch 'master' into discovery 2017-02-10 16:42:15 -05:00
Jarrod Johnson 5f1a2ed69c Add some helpful output delimiters to unit test 2017-02-10 15:47:13 -05:00
Jarrod Johnson 0668b90ba9 Improve the unit test for mac map
Actually get to using the config manager.
2017-02-10 15:45:58 -05:00
Jarrod Johnson fa4a759bc1 Start effort to consolidate snoop open scan
Scan and pass results as snoop answers.  They should
be like snoop answers, but with extra detail that will
be discarded.
2017-02-09 17:03:05 -05:00
Jarrod Johnson 890e23f2d9 Improve symmetry of scan and snoop in SLP
addresses versus peers, hwaddr versus mac, and services versus service
2017-02-09 14:59:01 -05:00
Jarrod Johnson e08a79b067 Update copyright year 2017-02-09 14:46:56 -05:00
Jarrod Johnson 0dc42567c2 Add a pxe-client service
For consistency with other plugins, model a pxe request as a 'pxe-client'
service being detected
2017-02-09 14:41:11 -05:00
Jarrod Johnson 94a3e83b04 Fix for multicast on multiple nics (SSDP)
It turns out that binding to INADDR_ANY doesn't work right for
multicast.

Workaround by adding membership for each ip.
2017-02-09 14:39:02 -05:00
Jarrod Johnson 7f71d01c2d Fix for multicast on multiple nics
It turns out that binding to INADDR_ANY doesn't work right for
multicast.

Workaround by adding membership for each ip.
2017-02-09 13:46:14 -05:00
Jarrod Johnson bd31358649 Support XCC 2017-02-08 16:42:20 -05:00
Jarrod Johnson be054689f8 Prepare for future 2017-02-08 16:41:16 -05:00
Jarrod Johnson 4fada9b931 Fix issue when no handler specified 2017-02-08 16:39:24 -05:00
Jarrod Johnson 795e4c1273 Merge branch 'master' into discovery 2017-02-07 13:45:33 -05:00
Jarrod Johnson 9174b46f66 Avoid mac address table lookups for common static
Some IP addresses are commonly used as static values.
In such a case, inferring a nice, clean 1:1 mac:ip relationship
can end in tears.  For those, don't even pretend to know the
associated mac address, even if the neighbor table would have
it.
2017-01-18 16:31:53 -05:00
Jarrod Johnson 1f3c93d7ee Merge branch 'master' into discovery 2017-01-18 15:38:41 -05:00
Jarrod Johnson c85d02d889 Merge branch 'master' into discovery 2017-01-12 07:41:25 -05:00
Jarrod Johnson 8c7c2026d9 Merge branch 'discovery' of github.com:jjohnson42/confluent into discovery 2017-01-03 15:48:37 -05:00
Jarrod Johnson e847a5b871 Properly handle SNMPv1 community only
If given SNMPv1 community only, use the data
correctly.
2016-11-29 10:45:16 -05:00
Jarrod Johnson c022466bf8 Merge branch 'discovery' of github.com:jjohnson42/confluent into discovery 2016-11-29 10:35:32 -05:00
Jarrod Johnson e03e9e0736 Have core actually start using the modules
This allows testing of the concept in a more complete form.
2016-11-16 16:39:12 -05:00
Jarrod Johnson 0caa7b5519 Consolidate interface for scan
Make the discovery modules implement a more
consistent interface strategy.
2016-11-16 16:02:54 -05:00
Jarrod Johnson c5f1e60414 Aggregate SSDP scan replies
Like in the SLP code, make efforts to group responses about the same peer together.
2016-11-16 13:00:15 -05:00
Jarrod Johnson 1f5c7208b8 Add a standalone test for slp
Allow individual test of the slp snoop
functionality.
2016-11-16 09:00:10 -05:00
Jarrod Johnson 3edba0f8bb Refactor neigh table handling and expand SSDP
SSDP support snoop is implemented.
2016-11-15 15:57:55 -05:00
Jarrod Johnson 786bf00521 Extend SSDP to listen
This lays the groundwork to implement notifying an application of
SSDP entries coming and going on a network.
2016-11-14 16:37:16 -05:00
Jarrod Johnson b944357071 Extract the client architecture from PXE
Since PXE contains client architecture, this may
one day be a useful piece of data to control boot.
Put it in now while the code is fresh in my mind.
2016-11-10 17:02:04 -05:00
Jarrod Johnson 59a7b190d5 Implement PXE snoop
This one only returns MAC and UUID, so the handler
must be special, compared to the ones that actually
identify an IP to interrogate.
2016-11-10 16:30:11 -05:00
Jarrod Johnson bd364ded70 Add notations for eventually doing pxe
This will for now be a 'snoop only' discovery.  One day this may also serve
for controlling PXE when DHCP sends an IP (we'd use 4011) and for static
hosts.
2016-11-10 09:55:06 -05:00
Jarrod Johnson da74830353 Comment in a sample of an SSDP notify
For use when implementing 'snoop' functionality.
2016-11-09 16:52:45 -05:00
Jarrod Johnson fff0488f89 Initial SSDP work
Refactor relevant bits of SLP implementation and do SSDP.
Note it still needs the monitoring for SSDP NOTIFY packets.
Unfortunately, this is another protocol that embeds the
address into the packet, making it tricky to have a sane
reusable function for the v4/v6 multicast/broadcast scheme.
2016-11-09 16:49:47 -05:00
Jarrod Johnson 63431deffb Merge branch 'master' into discovery 2016-11-09 10:40:06 -05:00
Jarrod Johnson a1757571b4 Write an SLP plugin, document flow in core
This serves as the framework that will ultimately feed
the 'detected' collection and manage the automation of
flow from being detected to a fully configured and managed
endpoint.
2016-10-27 16:49:58 -04:00
Jarrod Johnson 3ecb7aaf83 Move discovery into a subtree
This will make better room for providing a
suite of discovery protocols (namely slp,
ssdp, and mdns).
2016-10-27 16:30:10 -04:00
Jarrod Johnson 1ee166ea4e Tweak the proposed autonode attributes
Provide a pair of attributes for an enclosure class
device distinct from a directly connected server.
2016-10-27 16:27:31 -04:00
Jarrod Johnson 8e15dee11d Merge branch 'master' of github.com:xcat2/confluent into discovery 2016-10-27 16:14:05 -04:00
Jarrod Johnson 3610b16a6c Merge branch 'master' into discovery 2016-10-06 14:48:22 -04:00
Jarrod Johnson 4d50007377 Rework the attribute names for node generation
Accomodate likely scenario of mixing and matching direct rackmount and
enclosure based systems.
2016-10-04 14:39:38 -04:00
Jarrod Johnson d3e2af0215 Merge branch 'master' into discovery 2016-10-04 13:23:11 -04:00
Jarrod Johnson cff71ad95b Merge branch 'discovery' of github.com:jjohnson42/confluent into discovery 2016-10-04 13:13:29 -04:00
Jarrod Johnson edeef69ae9 Put out some concept autonode attributes
Something to put forth as a framework for the derivation
of generated node attributes.  One key gap is handling at
least one level of recursion so that a generated enclosure
manager can generate nodes.  Will probably steer clear of
arbitrarily deep recursion (switch can generate switch, etc).
2016-09-23 13:49:35 -04:00
Jarrod Johnson 4505e582b2 Add LLDP component
LLDP is to play a bigger rolle in confluent discovery.
This provides the starter module to gather such data.
2016-09-23 13:49:35 -04:00
Jarrod Johnson cf4fda2f7d Flesh out some of the design points
Mostly adding more comments in the areas that
will be implementing the design so that I'll
remember what I had in mind.
2016-09-23 13:49:35 -04:00
Jarrod Johnson 770559040e Add discovery core skeleton
This adds what will become the hub of discovery flow management,
as documented in the file's comments.
2016-09-23 13:49:35 -04:00
Jarrod Johnson 711061347c Begin adding hooks to work discovery
Discovery will be more explicitly organized.  This should
make it easier for folks to understand and have common paths
for embracing new schemes as appropriate.  At this point
PXE, SLP, SSDP, and mDNS are likely discovery schemes, and
'location' providers would be Ethernet switches and server
blade enclosure managers.
2016-09-23 13:49:35 -04:00
Jarrod Johnson 25169c2838 Defer console startup until after API startup
Consoles starting up would potentially delay API availaility.  Change
by having the API having ample time to startup, then commence the
busy work of starting cnosole sessions.
2016-09-13 11:33:25 -04:00
Jarrod Johnson 9685870c14 Merge branch 'master' into discovery 2016-09-09 09:15:25 -04:00
Jarrod Johnson 8221760fbd Put out some concept autonode attributes
Something to put forth as a framework for the derivation
of generated node attributes.  One key gap is handling at
least one level of recursion so that a generated enclosure
manager can generate nodes.  Will probably steer clear of
arbitrarily deep recursion (switch can generate switch, etc).
2016-09-01 17:46:51 -04:00
Jarrod Johnson 673785f576 Add LLDP component
LLDP is to play a bigger rolle in confluent discovery.
This provides the starter module to gather such data.
2016-09-01 11:32:30 -04:00
Jarrod Johnson 51ae476077 Flesh out some of the design points
Mostly adding more comments in the areas that
will be implementing the design so that I'll
remember what I had in mind.
2016-08-24 16:44:07 -04:00
Jarrod Johnson 8f7199bce6 Add discovery core skeleton
This adds what will become the hub of discovery flow management,
as documented in the file's comments.
2016-08-24 15:50:52 -04:00
Jarrod Johnson d282db0c02 Begin adding hooks to work discovery
Discovery will be more explicitly organized.  This should
make it easier for folks to understand and have common paths
for embracing new schemes as appropriate.  At this point
PXE, SLP, SSDP, and mDNS are likely discovery schemes, and
'location' providers would be Ethernet switches and server
blade enclosure managers.
2016-08-24 15:48:49 -04:00
135 changed files with 7753 additions and 10921 deletions
-114
View File
@@ -1,114 +0,0 @@
#!/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('-b', '--base',
help='Use given node as reference for comparison when '
'using -d, instead of using the most common result')
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')
argparser.add_option('-l', '--log', action='store', type='string', dest='log',
help='Log each output to file, using {node} as a placeholder for node.')
(options, args) = argparser.parse_args()
if sys.stdin.isatty():
argparser.print_help()
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, basenode=options.base)
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
if options.log:
node, output = line.split(': ', 1)
currlog = options.log.format(node=node, nodename=node)
with open(currlog, mode='a') as log:
log.write(output + '\n')
continue
grouped.add_line(*line.split(': ', 1))
if options.watch:
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()
+136 -218
View File
@@ -2,7 +2,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2018 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -49,7 +49,6 @@ import select
import shlex
import signal
import socket
import struct
import sys
import time
try:
@@ -77,7 +76,6 @@ import confluent.tlvdata as tlvdata
import confluent.client as client
conserversequence = '\x05c' # ctrl-e, c
clearpowermessage = False
oldtcattr = None
fd = sys.stdin
@@ -90,11 +88,6 @@ netserver = None
laststate = {}
class BailOut(Exception):
def __init__(self, errorcode=0):
self.errorcode = errorcode
def print_help():
print("confetty provides a filesystem like interface to confluent. "
"Navigation is done using the same commands as would be used in a "
@@ -122,7 +115,6 @@ def print_help():
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
status = consolename
info = []
for statekey in stateinfo:
@@ -137,15 +129,6 @@ 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:
@@ -160,7 +143,7 @@ def updatestatus(stateinfo={}):
info.append(time.strftime('%H:%M', time.localtime(showtime)))
if info:
status += ' [' + ','.join(info) + ']'
if os.environ.get('TERM', '') not in ('linux'):
if os.environ['TERM'] not in ('linux'):
sys.stdout.write('\x1b]0;console: %s\x07' % status)
sys.stdout.flush()
@@ -192,7 +175,7 @@ def recurse_format(datum, levels=0):
def prompt():
if os.environ.get('TERM', '') not in ('linux'):
if os.environ['TERM'] not in ('linux'):
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
try:
return raw_input(target + ' -> ')
@@ -301,17 +284,14 @@ def print_result(res):
if 'databynode' in res:
print_result(res['databynode'])
return
for key in sorted(res):
for key in res.iterkeys():
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):
try:
print '{0}: {1}'.format(key, res[key])
except UnicodeEncodeError:
print '{0}: {1}'.format(key, repr(res[key]))
print '{0}: {1}'.format(key, res[key])
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
@@ -357,9 +337,9 @@ def do_command(command, server):
return
argv[0] = argv[0].lower()
if argv[0] == 'exit':
if os.environ.get('TERM', '') not in ('linux'):
if os.environ['TERM'] not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
raise Bailout()
sys.exit(0)
elif argv[0] in ('help', '?'):
return print_help()
elif argv[0] == 'cd':
@@ -440,10 +420,6 @@ def do_command(command, server):
currconsole = targpath
startrequest = {'operation': 'start', 'path': targpath,
'parameters': {}}
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
startrequest['parameters']['width'] = width
startrequest['parameters']['height'] = height
for param in argv[2:]:
(parmkey, parmval) = param.split("=")
startrequest['parameters'][parmkey] = parmval
@@ -484,12 +460,8 @@ def createresource(args):
if keydata is None:
return
targpath = fullpath_target(resname)
if targpath.startswith('/noderange//'):
collection = targpath
else:
collection, _, resname = targpath.rpartition('/')
if 'name' not in keydata:
keydata['name'] = resname
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
makecall(session.create, (collection, keydata))
@@ -586,20 +558,11 @@ def fullpath_target(currpath, forcepath=False):
ntarget += '/'
return ntarget
def do_resize(a, b):
if not inconsole:
return
height, width = struct.unpack(
'hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'....'))[:2]
tlvdata.send(session.connection, {'operation': 'resize', 'width': width,
'height': height})
def startconsole(nodename):
global inconsole
global consolename
global didconsole
signal.signal(signal.SIGWINCH, do_resize)
didconsole = True
consolename = nodename
tty.setraw(sys.stdin.fileno())
@@ -618,12 +581,11 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
if oldtcattr is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
# Request default color scheme, to undo potential weirdness of terminal
if sys.stdout.isatty():
sys.stdout.write('\x1b[m')
sys.stdout.write('\x1b[m')
if fullexit:
if sys.stdout.isatty() and os.environ.get('TERM', '') not in ('linux'):
if os.environ['TERM'] not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
raise BailOut(code)
sys.exit(code)
else:
tlvdata.send(session.connection, {'operation': 'stop',
'path': currconsole})
@@ -747,23 +709,8 @@ def conserver_command(filehandle, localcommand):
else:
print("Unknown power state.]\r")
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)
#check_power_state()
elif localcommand[0] == '?':
print("help]\r")
print(". exit console\r")
@@ -774,7 +721,6 @@ 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")
@@ -784,10 +730,7 @@ def conserver_command(filehandle, localcommand):
def get_command_bytes(filehandle, localcommand, cmdlen):
while len(localcommand) < cmdlen:
try:
ready, _, _ = select.select((filehandle,), (), (), 1)
except select.error:
ready = ()
ready, _, _ = select.select((filehandle,), (), (), 1)
if ready:
localcommand += filehandle.read()
return localcommand
@@ -800,10 +743,7 @@ def check_escape_seq(currinput, filehandle):
sys.stdout.flush()
return conserver_command(
filehandle, currinput[len(conserversequence):])
try:
ready, _, _ = select.select((filehandle,), (), (), 3)
except select.error:
ready = ()
ready, _, _ = select.select((filehandle,), (), (), 3)
if not ready: # 3 seconds of no typing
break
currinput += filehandle.read()
@@ -816,10 +756,6 @@ parser.add_option("-s", "--server", dest="netserver",
parser.add_option("-c", "--control", dest="controlpath",
help="Path to offer terminal control",
metavar="PATH")
parser.add_option(
'-m', '--mintime', default=0,
help='Minimum time to run or else pause for input (used to keep a '
'terminal from closing quickly on error)')
opts, shellargs = parser.parse_args()
username = None
@@ -848,153 +784,135 @@ def server_connect():
passphrase = getpass.getpass("Passphrase: ")
session.authenticate(username, passphrase)
def check_power_state():
tlvdata.send(
session.connection,
{'operation': 'retrieve',
'path': '/nodes/' + consolename + '/power/state'})
return
try:
server_connect()
except EOFError, KeyboardInterrupt:
sys.exit(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
sys.exit(1)
# clear on start can help with readable of TUI, but it
# can be annoying, so for now don't do it.
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
if sys.stdout.isatty():
import readline
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
dl = readline.get_completer_delims().replace('-', '')
readline.set_completer_delims(dl)
readline.set_completer(completer)
def main():
global inconsole
try:
server_connect()
except EOFError, KeyboardInterrupt:
raise BailOut(0)
except socket.gaierror:
sys.stderr.write('Could not connect to confluent\n')
raise BailOut(1)
# clear on start can help with readable of TUI, but it
# can be annoying, so for now don't do it.
# sys.stdout.write('\x1b[H\x1b[J')
# sys.stdout.flush()
global powerstate, powertime, clearpowermessage
doexit = False
inconsole = False
pendingcommand = ""
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
command = " ".join(shellargs)
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
powertime = None
if sys.stdout.isatty():
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set bell-style none")
dl = readline.get_completer_delims().replace('-', '')
readline.set_completer_delims(dl)
readline.set_completer(completer)
doexit = False
inconsole = False
pendingcommand = ""
session_node = get_session_node(shellargs)
if session_node is not None:
consoleonly = True
do_command("start /nodes/%s/console/session" % session_node, netserver)
doexit = True
elif shellargs:
command = " ".join(shellargs)
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
powertime = None
while inconsole or not doexit:
if inconsole:
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
# case of a console session
# each command should slurp up all relevant
# recv potential
#fh.read()
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
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
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
sys.stdout.flush()
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
try:
myinput = fh.read()
myinput = check_escape_seq(myinput, fh)
if myinput:
tlvdata.send(session.connection, myinput)
except IOError:
pass
if powerstate is None or powertime < time.time() - 10: # Check powerstate every 10 seconds
powertime = time.time()
powerstate = True
check_power_state()
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:
currcommand = prompt()
sys.stdout.write(rsp)
sys.stdout.flush()
while inconsole or not doexit:
if inconsole:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 60)
for fh in rdylist:
if fh == session.connection:
# this only should get called in the
# case of a console session
# each command should slurp up all relevant
# recv potential
#fh.read()
try:
data = tlvdata.recv(fh)
except Exception:
data = None
if type(data) == dict:
updatestatus(data)
continue
if data is not None:
try:
sys.stdout.write(data)
except IOError: # Some times circumstances are bad
# resort to byte at a time...
for d in data:
sys.stdout.write(d)
now = time.time()
if ('showtime' not in laststate or
(now // 60) != laststate['showtime'] // 60):
# don't bother churning if minute does not change
laststate['showtime'] = now
updatestatus()
sys.stdout.flush()
else:
deadline = 5
connected = False
while not connected and deadline > 0:
try:
server_connect()
connected = True
except (socket.gaierror, socket.error):
pass
if not connected:
time.sleep(1)
deadline -=1
if connected:
do_command(
"start /nodes/%s/console/session skipreplay=True" % consolename,
netserver)
else:
doexit = True
inconsole = False
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
try:
myinput = fh.read()
myinput = check_escape_seq(myinput, fh)
if myinput:
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()
else:
currcommand = prompt()
try:
do_command(currcommand, netserver)
except socket.error:
try:
server_connect()
do_command(currcommand, netserver)
except socket.error:
try:
server_connect()
do_command(currcommand, netserver)
except socket.error:
doexit = True
sys.stdout.write('Lost connection to server')
quitconfetty(fullexit=True)
if __name__ == '__main__':
errcode = 0
deadline = 0
if opts.mintime:
deadline = os.times()[4] + float(opts.mintime)
try:
main()
except BailOut as e:
errcode = e.errorcode
finally:
if deadline and os.times()[4] < deadline:
sys.stderr.write('[Exited early, hit enter to continue]')
sys.stdin.readline()
sys.exit(errcode)
doexit = True
sys.stdout.write('Lost connection to server')
quitconfetty(fullexit=True)
-71
View File
@@ -1,71 +0,0 @@
#!/usr/bin/env python
import optparse
import signal
import sys
import os
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.sortutil as sortutil
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o ansible.hosts
\n ''')
argparser.add_option('-o', '--output',
help='Ansible hosts file')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not options.output:
sys.stderr.write('Output file must be specified by -o\n')
sys.exit(1)
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
nodesbygroup = {}
with open(options.output, 'w') as importfile:
needempty = False
for node in sortutil.natural_sort(databynode):
data = databynode[node]
if not data.get('groups', []):
importfile.write(node + '\n')
needempty = True
for g in data.get('groups', []):
if g not in nodesbygroup:
nodesbygroup[g] = set([node])
else:
nodesbygroup[g].add(node)
if needempty:
importfile.write('\n')
for group in sortutil.natural_sort(nodesbygroup):
importfile.write('[{0}]\n'.format(group))
for node in sortutil.natural_sort(nodesbygroup[group]):
importfile.write('{0}\n'.format(node))
importfile.write('\n')
if __name__ == '__main__':
main()
-76
View File
@@ -1,76 +0,0 @@
#!/usr/bin/env python
import csv
import optparse
import signal
import sys
import os
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.sortutil as sortutil
headers = '# Type,Serial Number,Current IP,Current username,Current password,New password,Recovery password,Switch enable password,New IPv4,IPv4 Subnet mask,IPv4 Default gateway,IPv4 DNS1,IPv4 DNS2,New IPv6,IPv6 Prefix,IPv6 Default gateway,IPv6 DNS1,IPv6 DNS2,Domain,Host name,Display name,Rack,Lowest Rack Unit,Height,Force,Stored credentials ID,Managed authentication,Group Name'.split(',')
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o bulkimport.csv
\n ''')
argparser.add_option('-o', '--output',
help='File to write for bulk import into xClarity Administrator')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not options.output:
sys.stderr.write('Output file must be specified by -o\n')
sys.exit(1)
if 'XCCUSER' not in os.environ or 'XCCPASS' not in os.environ:
sys.stderr.write('Must specify XCCUSER and XCCPASS in environment variables\n')
sys.exit(1)
xccuser = os.environ['XCCUSER']
xccpass = os.environ['XCCPASS']
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
for res in sess.read(
'/noderange/{0}/configuration/management_controller/net_interfaces/management'.format(
noderange)):
for node in res.get('databynode', {}):
currip = res['databynode'][node].get('ipv4_address', {}).get(
'value', '')
if currip:
databynode[node]['hardwaremanagement.manager'] = {
'value': currip.split('/', 1)[0]}
with open(options.output, 'w') as importfile:
bulkimport = csv.writer(importfile)
bulkimport.writerow(headers)
for node in sortutil.natural_sort(databynode):
data = databynode[node]
row = ['server', lookupdata(data, 'id.serial'), lookupdata(data, 'hardwaremanagement.manager'), xccuser, xccpass, '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', node, lookupdata(data, 'location.rack'), lookupdata(data, 'location.u'), '', '', '','', '']
bulkimport.writerow(row)
if __name__ == '__main__':
main()
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env python
import csv
import optparse
import signal
import sys
import os
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.sortutil as sortutil
def lookupdata(data, key):
ret = data.get(key, {}).get('value', '')
if ret is None:
ret = ''
return ret
def main():
argparser = optparse.OptionParser(
usage='''\n %prog noderange -o ansible.hosts
\n ''')
argparser.add_option('-o', '--output',
help='xCAT stanza file')
argparser.add_option('-m', '--macs',
help='xCAT macs.csv to write')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
if not (options.output or options.macs):
sys.stderr.write('Output file must be specified by -o or -m\n')
sys.exit(1)
sess = client.Command()
databynode = {}
for res in sess.read('/noderange/{0}/attributes/all'.format(noderange)):
for node in res.get('databynode', {}):
if node not in databynode:
databynode[node] = {}
databynode[node].update(res['databynode'][node])
if options.output:
with open(options.output, 'w') as importfile:
for node in sortutil.natural_sort(databynode):
xcatattrs = collect_attribute_data(databynode, node)
importfile.write('{0}:\n'.format(node))
importfile.write(' objtype=node\n')
importfile.write(' arch=x86_64\n')
importfile.write(' netboot=xnba\n')
importfile.write(' mgt=ipmi\n')
for attr in xcatattrs:
if xcatattrs[attr] is None:
continue
importfile.write(' {0}={1}\n'.format(attr,
xcatattrs[attr]))
importfile.write('\n')
if options.macs:
with open(options.macs, 'w') as importfile:
macsv = csv.writer(importfile)
macsv.writerow(['#node', 'mac'])
for node in sortutil.natural_sort(databynode):
xcatattrs = collect_attribute_data(databynode, node)
macsv.writerow([node, xcatattrs['mac']])
def collect_attribute_data(databynode, node):
data = databynode[node]
xcatattrs = {}
xcatattrs['groups'] = ','.join(data.get('groups', []))
xcatattrs['bmc'] = data.get('hardwaremanagement.manager', {}).get(
'value', None)
xcatattrs['mpa'] = data.get('enclosure.manager', {}).get(
'value', None)
xcatattrs['slotid'] = data.get('enclosure.bay', {}).get(
'value', None)
gotmac = False
for key in data:
if key.startswith('net.') and 'hwaddr' in key:
currmac = data[key].get('value', None)
if currmac:
if gotmac:
sys.stderr.write(
'Ignoring {0} and using only {1} for mac, '
'multiple macs not supported by '
'confluent2xcat\n'.format(key, gotmac))
continue
gotmac = key
xcatattrs['mac'] = currmac
return xcatattrs
if __name__ == '__main__':
main()
-19
View File
@@ -1,19 +0,0 @@
#!/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/* ::
+3 -27
View File
@@ -17,7 +17,6 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -36,20 +35,14 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog [-b] noderange [list of attributes or 'all'] \
usage='''\n %prog [-b] noderange [list of attributes] \
\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')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
@@ -62,7 +55,6 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
@@ -70,24 +62,8 @@ exitcode = 0
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment or options.prompt:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
exitcode=client.updateattrib(session,args,nodetype, noderange, options, argassign)
if "=" in args[1] or options.clear:
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
try:
# setting user output to what the user inputs
if args[1] == 'all':
-62
View File
@@ -1,62 +0,0 @@
#!/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)
+6 -11
View File
@@ -35,9 +35,6 @@ argparser = optparse.OptionParser()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-u', '--uefi', dest='uefimode',
action='store_true', default=False,
help='Request UEFI style boot (rather than BIOS)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
@@ -52,7 +49,6 @@ 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]
@@ -65,12 +61,11 @@ if options.biosmode:
else:
bootmode = 'uefi'
errnodes = set([])
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist,
errnodes=errnodes)
if errnodes:
noderange = noderange + ',-(' + ','.join(errnodes) + ')'
rc |= session.simple_noderange_command(noderange, '/power/state', 'boot')
sys.exit(rc)
persistent=options.persist)
if rc:
sys.exit(rc)
else:
sys.exit(session.simple_noderange_command(noderange, '/power/state', 'boot'))
-263
View File
@@ -1,263 +0,0 @@
#!/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 shlex
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(usage="Usage: %prog [options] noderange [option|option=value]")
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('-b', '--batch', dest='batch', metavar='settings.batch',
default=False, help='Provide settings in a batch file')
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')
argparser.add_option('-a', '--advanced', dest='advanced',
action='store_true', default=False,
help='Include advanced settings, which are normally not '
'intended to be used without direction from the '
'relevant server vendor.')
argparser.add_option('-r', '--restoredefault', default=False,
dest='restoredefault', metavar="COMPONENT",
help='Restore the configuration of the node '
'to factory default for given component. '
'Currently only uefi is supported')
(options, args) = argparser.parse_args()
cfgpaths = {
'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
def parse_config_line(arguments):
global setmode, forceset, key, value, needval, candidate, path, attrib
for param in arguments:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
setmode = True
forceset = True
continue
if needval:
key = needval
needval = None
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
if options.batch:
printsys = []
argfile = open(options.batch, 'r')
argset = argfile.readline()
while argset:
try:
argset = argset[:argset.index('#')]
except ValueError:
pass
argset = argset.strip()
if argset:
parse_config_line(shlex.split(argset))
argset = argfile.readline()
else:
parse_config_line(args[1:])
session = client.Command()
rcode = 0
if options.restoredefault and options.restoredefault.lower() in (
'sys', 'system', 'uefi', 'bios'):
for fr in session.update(
'/noderange/{0}/configuration/system/clear'.format(noderange),
{'clear': True}):
rcode |= client.printerror(fr)
sys.exit(rcode)
elif options.restoredefault:
sys.stderr.write(
'Unrecognized component to restore defaults: {0}\n'.format(
options.restoredefault))
sys.exit(1)
if setmode:
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
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 = []
if (options.comparedefault or printsys == []) and not options.advanced:
path = '/noderange/{0}/configuration/system/all'.format(noderange)
else:
path = '/noderange/{0}/configuration/system/advanced'.format(
noderange)
rcode = client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
+2 -45
View File
@@ -17,14 +17,7 @@
import optparse
import os
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
argparser = optparse.OptionParser(
@@ -33,45 +26,9 @@ argparser = optparse.OptionParser(
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
"For example, ctrl-'e', then 'c', then '.' will exit the current "
"console")
argparser.add_option('-t', '--tile', action='store_true', default=False,
help='Tile console windows in the terminal')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
if options.tile:
null = open('/dev/null', 'w')
nodes = []
sess = client.Command()
for res in sess.read('/noderange/{0}/nodes/'.format(args[0])):
node = res.get('item', {}).get('href', '/').replace('/', '')
if not node:
sys.stderr.write(res.get('error', repr(res)) + '\n')
sys.exit(1)
nodes.append(node)
initial = True
pane = 0
for node in sortutil.natural_sort(nodes):
if initial:
initial = False
subprocess.call(
['tmux', 'new-session', '-d', '-s',
'nodeconsole_{0}'.format(os.getpid()), '-x', '800', '-y',
'800', '{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
else:
subprocess.call(['tmux', 'select-pane', '-t', str(pane)])
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
pane += 1
subprocess.call(
['tmux', 'split', '-h',
'{0} -m 5 start /nodes/{1}/console/session'.format(
confettypath, node)])
subprocess.call(['tmux', 'select-layout', 'tiled'], stdout=null)
subprocess.call(['tmux', 'select-pane', '-t', '0'])
subprocess.call(['tmux', 'set-option', 'pane-border-status', 'top'], stderr=null)
os.execlp('tmux', 'tmux', 'attach', '-t', 'nodeconsole_{0}'.format(
os.getpid()))
else:
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
os.execl(confettypath, confettypath, 'start',
'/nodes/{0}/console/session'.format(args[0]))
-58
View File
@@ -1,58 +0,0 @@
#!/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)
-351
View File
@@ -1,351 +0,0 @@
#!/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
import time
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.sortutil as sortutil
defcolumns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
columnmapping = {
'Node': 'nodename',
'Model': 'modelnumber',
'Serial': 'serialnumber',
'UUID': 'uuid',
'Type': 'types',
'Current IP Addresses': 'ipaddrs',
'IP': 'ipaddrs',
'Bay': 'bay',
'Mac Address': 'macs',
'Mac': 'macs',
'Switch': 'switch',
'Port': 'port',
'Advertised IP': 'otheripaddrs',
'Other IP': 'otheripaddrs',
}
def print_disco(options, session, currmac, outhandler, columns):
procinfo = {}
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
if 'Switch' in columns or 'Port' in columns:
for tmpinfo in session.read(
'/networking/macs/by-mac/{0}'.format(currmac)):
if 'ports' in tmpinfo:
# The api sorts so that the most specific available value
# is last
procinfo.update(tmpinfo['ports'][-1])
record = []
for col in columns:
rawcol = columnmapping[col]
rawval = procinfo.get(rawcol, '')
if isinstance(rawval, list):
record.append(','.join(rawval))
else:
record.append(str(rawval))
outhandler.add_row(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.ipv4_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
unique_fields = frozenset(['serial', 'mac', 'uuid'])
def import_csv(options, session):
nodedata = []
unique_data = {}
with open(options.importfile, 'r') as datasrc:
records = csv.reader(datasrc)
fields = process_header(next(records))
for field in fields:
if field in unique_fields:
unique_data[field] = set([])
for record in records:
currfields = list(fields)
nodedatum = {}
for datum in record:
currfield = currfields.pop(0)
if currfield in unique_fields:
if datum in unique_data[currfield]:
sys.stderr.write(
"Import contains duplicate values "
"({0} with value {1}\n".format(currfield, datum)
)
sys.exit(1)
unique_data[currfield].add(datum)
nodedatum[currfield] = datum
if not datum_complete(nodedatum):
sys.exit(1)
if not search_record(nodedatum, options, session):
blocking_scan(session)
if not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
sys.exit(1)
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):
orderby = None
if options.fields:
columns = []
for field in options.fields.split(','):
for cdt in columnmapping:
if cdt.lower().replace(
' ', '') == field.lower().replace(' ', ''):
columns.append(cdt)
else:
columns = defcolumns
if options.order:
for field in columns:
if options.order.lower() == field.lower():
orderby = field
outhandler = client.Tabulator(columns)
for mac in list_matching_macs(options, session):
print_disco(options, session, mac, outhandler, columns)
if options.csv:
outhandler.write_csv(sys.stdout, orderby)
else:
for row in outhandler.get_table(orderby):
print(row)
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:
# Do a rescan to catch missing requested data
blocking_scan(session)
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 blocking_scan(session):
list(session.update('/discovery/rescan', {'rescan': 'start'}))
while(list(session.read('/discovery/rescan'))[0].get('scanning', False)):
time.sleep(0.5)
list(session.update('/networking/macs/rescan', {'rescan': 'start'}))
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')
parser.add_option('-f', '--fields', dest='fields',
help='Select fields for output',
metavar='FIELDS')
parser.add_option('-o', '--order', dest='order',
help='Order output by given field', metavar='ORDER')
(options, args) = parser.parse_args()
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
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':
blocking_scan(session)
print("Rescan complete")
if __name__ == '__main__':
main()
+1 -10
View File
@@ -15,7 +15,6 @@
# 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
@@ -33,8 +32,6 @@ 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()
@@ -43,17 +40,11 @@ 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
+30 -106
View File
@@ -19,7 +19,6 @@ import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
@@ -31,20 +30,32 @@ 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 and data['model']:
if 'model' in data:
prefix += ' ' + data['model']
builddesc = []
if 'build' in data and data['build']:
if 'build' in data:
builddesc.append(data['build'])
if 'date' in data and data['date']:
if 'date' in data:
builddesc.append(data['date'])
if 'version' in data and data['version']:
if 'version' in data:
version = data['version']
if builddesc:
version += ' ({0})'.format(' '.join(builddesc))
@@ -52,115 +63,28 @@ 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> [list][update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
upfile = None
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'update':
upfile = args[2]
else:
if args[1] == 'list':
comps = args[2:]
else:
comps = args[1:]
components = []
for arg in comps:
components += arg.split(',')
if not components:
components = ['all']
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()
if upfile is None:
show_firmware(session)
else:
update_firmware(session, upfile)
for res in session.read('/noderange/{0}/inventory/firmware/all/all'.format(
noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
if 'firmware' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['firmware']:
for prefix in inv:
printfirm(node, prefix, inv[prefix])
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+4 -28
View File
@@ -17,7 +17,6 @@
__author__ = 'alin37'
from getpass import getpass
import optparse
import os
import signal
@@ -36,20 +35,14 @@ import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog [options] \
\n %prog [options] nodegroup [list of attributes|all] \
\n %prog [options] nodegroup nodes=value1,value2 \
\n %prog -e nodegroup <attribute names to set> \
\n %prog [options] nodegroup nodes=value1,value2
\n %prog [options] nodegroup [list of attributes] \
\n %prog [options] nodegroup group=value1,value2 \
\n %prog [options] nodegroup group=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')
argparser.add_option('-p', '--prompt', action='store_true',
help='Prompt for attribute values interactively')
(options, args) = argparser.parse_args()
@@ -63,7 +56,6 @@ try:
nodelist = '/{0}/{1}/'.format(nodetype,nodegroups)
except IndexError:
nodelist = '/nodegroups/'
client.check_globbing(nodegroups)
session = client.Command()
exitcode = 0
@@ -71,23 +63,7 @@ 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)
argassign = None
if options.prompt:
argassign = {}
for arg in args[1:]:
oneval = 1
twoval = 2
while oneval != twoval:
oneval = getpass('Enter value for {0}: '.format(arg))
twoval = getpass('Confirm value for {0}: '.format(arg))
if oneval != twoval:
print('Values did not match.')
argassign[arg] = twoval
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options, argassign)
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
try:
# setting user output to what the user inputs
if args[1] == 'all':
-58
View File
@@ -1,58 +0,0 @@
#!/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)
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2,alin37,andywray'
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
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog\n")
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
nodelist = '/nodegroups/'
session = client.Command()
exitcode = 0
showtype='all'
requestargs=args[1:]
nodetype='noderange'
if len(args) > 0:
argparser.print_help()
sys.exit(1)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(res['item']['href'].replace('/', ''))
sys.exit(exitcode)
if __name__ == '__main__':
main()
-52
View File
@@ -1,52 +0,0 @@
#!/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)
-63
View File
@@ -1,63 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2,alin37,andywray'
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
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog <group> <new group name>\n")
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
nodelist = '/nodegroups/'
session = client.Command()
exitcode = 0
requestargs=args[1:]
nodetype='noderange'
if len(args) != 2:
argparser.print_help()
sys.exit(1)
else:
for res in session.update(
'/nodegroups/{0}/attributes/rename'.format(args[0]),
{'rename': args[1]}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print('{0}: {1}'.format(res['oldname'], res['newname']))
sys.exit(exitcode)
if __name__ == '__main__':
main()
+2 -12
View File
@@ -41,16 +41,8 @@ try:
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
codemappings = {
'ok': 0,
'warning': 1,
'critical': 2,
'failed': 2,
}
def main():
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
session = client.Command()
@@ -73,11 +65,9 @@ def main():
if 'error' in health[node]:
sys.stderr.write('{0}: Error: {1}\n'.format(
node, health[node]['error']))
exitcode = 3
exitcode |= 1
if 'health' in health[node]:
healthbynode[node] = health[node]['health']['value']
if codemappings[healthbynode[node]] > exitcode:
exitcode = codemappings[healthbynode[node]]
if 'sensors' in health[node]:
healthexplanations[node] = []
for sensor in health[node]['sensors']:
@@ -103,5 +93,5 @@ try:
main()
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
sys.exit(0)
+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(
+10 -73
View File
@@ -15,11 +15,8 @@
# 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
@@ -34,10 +31,6 @@ if path.startswith('/opt'):
import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
def pretty(text):
if text == 'pcislot':
@@ -47,17 +40,14 @@ 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))
@@ -86,41 +76,19 @@ def printerror(res, node=None):
exitcode = 1
url = '/noderange/{0}/inventory/hardware/all/all'
usedprefixes = set([])
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
argparser.add_option('-j', '--json', action='store_true', help='Output JSON')
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.exit(1)
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'
if len(args) > 1 and args[1] == 'firm':
os.execlp('nodefirmware', 'nodefirmware', noderange)
try:
if options.json:
databynode = {}
session = client.Command()
for res in session.read(url.format(noderange)):
for res in session.read('/noderange/{0}/inventory/hardware/all/all'.format(
noderange)):
printerror(res)
if 'databynode' not in res:
continue
@@ -130,19 +98,8 @@ try:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
idx = 2
while (node, prefix) in usedprefixes:
prefix = '{0} {1}'.format(inv['name'], idx)
idx += 1
usedprefixes.add((node, prefix))
if not inv['present']:
if not filters:
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
else:
print '{0}: {1}: Not Present'.format(node, prefix)
print '{0}: {1}: Not Present'.format(node, prefix)
continue
info = inv['information']
info.pop('board_extra', None)
@@ -150,34 +107,14 @@ try:
info.pop('chassis_extra', None)
info.pop('product_extra', None)
if 'memory_type' in info:
if not filters:
if options.json:
if node not in databynode:
databynode[node] = {}
databynode[node][prefix] = inv
continue
print_mem_info(node, prefix, info)
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)
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import 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
exitcode = 0
argparser = optparse.OptionParser(
usage="Usage: "
"%prog <noderange> [list][install <file>]")
(options, args) = argparser.parse_args()
upfile = None
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'install':
upfile = args[2]
else:
components = ['all']
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def install_license(session, filename):
global exitcode
resource = '/noderange/{0}/configuration/' \
'management_controller/licenses/'.format(noderange)
filename = os.path.abspath(filename)
instargs = {'filename': filename}
for res in session.create(resource, instargs):
pass # print(repr(res))
show_licenses()
def show_licenses(session):
global exitcode
firmware_shown = False
for res in session.read(
'/noderange/{0}/configuration/management_controller/licenses/'
'all'.format(noderange)):
for node in res.get('databynode', {}):
for license in res['databynode'][node].get('License', []):
print('{0}: {1}'.format(node, license.get('feature',
'Unknown')))
try:
session = client.Command()
if upfile is None:
show_licenses(session)
else:
install_license(session, upfile)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
-1
View File
@@ -47,7 +47,6 @@ 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
@@ -1,190 +0,0 @@
#!/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()
+6 -23
View File
@@ -34,22 +34,19 @@ 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(args) > 1:
if len(sys.argv) > 2:
if setstate == 'softoff':
setstate = 'shutdown'
elif not args[1] in ('stat', 'state', 'status'):
setstate = args[1]
elif not sys.argv[2] in ('stat', 'state', 'status'):
setstate = sys.argv[2]
if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
argparser.print_help()
@@ -57,19 +54,5 @@ if setstate not in (None, 'on', 'off', 'shutdown', 'boot', 'reset'):
session = client.Command()
exitcode = 0
session.add_precede_key('oldstate')
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))
sys.exit(
session.simple_noderange_command(noderange, '/power/state', setstate))
-52
View File
@@ -1,52 +0,0 @@
#!/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)
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2019 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import 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> <newname>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
identifystate = None
if len(sys.argv) > 2:
newname = sys.argv[2]
else:
argparser.print_help()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
session.simple_noderange_command(noderange, 'attributes/rename', newname))
-62
View File
@@ -1,62 +0,0 @@
#!/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)
-153
View File
@@ -1,153 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from collections import deque
import optparse
import os
import select
import shlex
import signal
import subprocess
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
import confluent.screensqueeze as sq
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog location noderange:location",
)
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of nodes to concurrently rsync')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# 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 or ':' not in args[-1]:
argparser.print_help()
sys.exit(1)
concurrentprocs = options.count
noderange, targpath = args[-1].split(':', 1)
client.check_globbing(noderange)
c = client.Command()
cmdstr = " ".join(args[:-1])
cmdstr = 'rsync -av --info=progress2 ' + cmdstr
cmdstr += ' {node}:' + targpath
currprocs = 0
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
for exp in c.create('/noderange/{0}/attributes/expression'.format(noderange),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
rdy, _, _ = select.select(all, [], [], 10)
nodeerrs = {}
pernodeout = {}
pernodefile = {}
output = sq.ScreenPrinter(noderange, c)
while all:
for r in rdy:
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0)[0]:
data = r.read(1)
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = ''
pernodeout[node] += data
if '\n' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\n', 1)
if currout:
pernodefile[node] = os.path.basename(currout)
if '\r' in pernodeout[node]:
currout, pernodeout[node] = pernodeout[node].split('\r', 1)
if currout:
currout = currout.split()
try:
currout = currout[1]
output.set_output(node, '{0}:{1}'.format(pernodefile[node], currout))
except IndexError:
pernodefile = currout[0]
pass
else:
output.set_output(node, 'error!')
if node not in nodeerrs:
nodeerrs[node] = ''
nodeerrs[node] += data
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if node not in nodeerrs:
output.set_output(node, 'complete')
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
if all:
rdy, _, _ = select.select(all, [], [], 10)
for node in nodeerrs:
for line in nodeerrs[node].split('\n'):
sys.stderr.write('{0}: {1}\n'.format(node, line))
sys.exit(exitcode)
def run_cmdv(node, cmdv, all, pipedesc):
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
pipedesc[nopen.stdout] = {'node': node, 'popen': nopen,
'type': 'stdout'}
pipedesc[nopen.stderr] = {'node': node, 'popen': nopen,
'type': 'stderr'}
all.add(nopen.stdout)
all.add(nopen.stderr)
if __name__ == '__main__':
run()
+24 -51
View File
@@ -34,27 +34,21 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog [options] 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')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
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",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
concurrentprocs = options.count
client.check_globbing(args[0])
c = client.Command()
cmdstr = " ".join(args[1:])
@@ -62,14 +56,10 @@ def run():
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
ex = exp['databynode']
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
@@ -78,45 +68,28 @@ def run():
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
exitcode = 0
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
data = r.readline()
desc = pipedesc[r]
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:
if options.nonodeprefix:
sys.stderr.write(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
else:
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]:
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
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)
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
@@ -134,4 +107,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
if __name__ == '__main__':
run()
run()
+1 -5
View File
@@ -36,7 +36,6 @@ 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',
@@ -65,7 +64,6 @@ if options.numreadings:
options.interval = 1
try:
noderange = args[0]
client.check_globbing(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
@@ -80,6 +78,7 @@ 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 = {}
@@ -126,8 +125,6 @@ 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''):
@@ -149,7 +146,6 @@ 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
+3 -4
View File
@@ -48,13 +48,12 @@ 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)
bootdev = None
if len(args) > 1:
bootdev = args[1]
if len(sys.argv) > 2:
bootdev = sys.argv[2]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
@@ -65,4 +64,4 @@ else:
bootmode = 'uefi'
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
persistent=options.persist))
+26 -54
View File
@@ -19,6 +19,7 @@ from collections import deque
import optparse
import os
import select
import shlex
import signal
import subprocess
import sys
@@ -33,28 +34,21 @@ if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog [options] 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')
argparser.add_option('-n', '--nonodeprefix', action='store_true',
help='Do not prefix output with node names')
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",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
cmdstr = " ".join(args[1:])
@@ -62,62 +56,40 @@ def run():
all = set([])
pipedesc = {}
pendingexecs = deque()
exitcode = 0
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
if 'error' in exp:
sys.stderr.write(exp['error'] + '\n')
exitcode |= exp.get('errorcode', 1)
ex = exp.get('databynode', ())
ex = exp['databynode']
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = ['ssh', node, cmd]
cmdv = ['ssh', node] + shlex.split(cmd)
if currprocs < concurrentprocs:
currprocs += 1
run_cmdv(node, cmdv, all, pipedesc)
else:
pendingexecs.append((node, cmdv))
if not all or exitcode:
sys.exit(exitcode)
exitcode = 0
rdy, _, _ = select.select(all, [], [], 10)
while all:
pernodeout = {}
for r in rdy:
data = r.readline()
desc = pipedesc[r]
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:
if options.nonodeprefix:
sys.stderr.write(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
else:
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]:
if options.nonodeprefix:
sys.stdout.write(line)
else:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
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)
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
@@ -135,4 +107,4 @@ def run_cmdv(node, cmdv, all, pipedesc):
if __name__ == '__main__':
run()
run()
-205
View File
@@ -1,205 +0,0 @@
#!/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
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
exitcode = 0
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def mbtohuman(mb):
if mb > 1000000:
return '{0:.3f} TB'.format(mb/1000000.0)
if mb > 1000:
return '{0:.3f} GB'.format(mb/1000.0)
return '{0:.3f} MB'.format(mb)
def showstorage(noderange, options, args):
global exitcode
session = client.Command()
disks = {}
arrays = {}
volumes = {}
scfg = session.read('/noderange/{0}/configuration/storage/all'.format(
noderange))
_print_cfg(scfg)
def _print_cfg(scfg):
global exitcode
storagebynode = {}
for e in scfg:
if 'error' in e:
sys.stderr.write(e['error'] + '\n')
exitcode = e.get('errorcode', 1)
for node in e.get('databynode', {}):
if node not in storagebynode:
storagebynode[node] = {'disks': [], 'arrays': [],
'volumes': []}
curr = e['databynode'][node]
storagebynode[node][curr['type'] + 's'].append(curr)
for node in storagebynode:
for disk in sorted(storagebynode[node]['disks'],
key=lambda x: x['name']):
print('{0}: Disk {1} Description: {2}'.format(
node, disk['name'], disk['description']))
print('{0}: Disk {1} State: {2}'.format(node, disk['name'],
disk['state']))
print('{0}: Disk {1} FRU: {2}'.format(node, disk['name'],
disk['fru']))
print('{0}: Disk {1} Serial Number: {2}'.format(node, disk['name'],
disk['serial']))
if disk['array']:
print('{0}: Disk {1} Array: {2}'.format(node, disk['name'],
disk['array']))
for arr in storagebynode[node]['arrays']:
print('{0}: Array {1} Available Capacity: {2}'.format(
node, arr['id'], mbtohuman(arr['available'])))
print('{0}: Array {1} Total Capacity: {2}'.format(
node, arr['id'], mbtohuman(arr['capacity'])))
print('{0}: Array {1} RAID: {2}'.format(node, arr['id'],
arr['raid']))
print('{0}: Array {1} Disks: {2}'.format(node, arr['id'], ','.join(
arr['disks'])))
print(
'{0}: Array {1} Volumes: {2}'.format(node, arr['id'], ','.join(
arr['volumes'])))
for vol in storagebynode[node]['volumes']:
print('{0}: Volume {1}: Size: {2}'.format(node, vol['name'],
mbtohuman(vol['size'])))
print('{0}: Volume {1}: State: {2}'.format(node, vol['name'],
vol['state']))
print('{0}: Volume {1}: Array {2}'.format(node, vol['name'],
vol['array']))
def createstorage(noderange, options, args):
if options.raidlevel is None or options.disks is None:
sys.stderr.write('-r and -d are required arguments to create array\n')
sys.exit(1)
session = client.Command()
names = options.name
if names is None:
names = ''.join(args)
parms = {'disks': options.disks, 'raidlevel': options.raidlevel,
'name': names}
if options.size:
parms['size'] = options.size
_print_cfg(session.create(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names), parms))
def deletestorage(noderange, options, args):
if options.name is None:
if len(args) == 1:
names = args[0]
else:
sys.stderr.write('-n is required to indicate volume(s) to delete\n')
sys.exit(1)
else:
names = options.name
session = client.Command()
for rsp in session.delete(
'/noderange/{0}/configuration/storage/volumes/{1}'.format(
noderange, names)):
if 'deleted' in rsp:
print('Deleted: {0}'.format(rsp['deleted']))
elif 'databynode' in rsp:
for node in rsp['databynode']:
if 'error' in rsp['databynode'][node]:
sys.stderr.write('{0}: {1}\n'.format(
node, rsp['databynode'][node]['error']))
else:
sys.stderr.write('{0}: {1}\n'.format(
node, repr(rsp['databynode'][node])))
else:
print(repr(rsp))
# def setstorage(noderange, options, args):
# pass
funmap = {
'create': createstorage,
'show': showstorage,
# 'set': setstorage,
'delete': deletestorage,
'rm': deletestorage,
}
def main():
argparser = OptParser(
usage='Usage: %prog <noderange> [show|create|delete]',
epilog='',
)
argparser.add_option('-r', '--raidlevel', type='int',
help='RAID level to use when creating an array')
argparser.add_option('-d', '--disks', type='str',
help='Comma separated list of disks to use, or the '
'word "rest" to indicate use of all available '
'disks')
argparser.add_option('-s', '--size', type='str',
help='Comma separated list of sizes to use when '
'creating volumes. The sizes may be absolute '
'size (e.g. 16gb), percentage (10%) or the word '
'"rest" to use remaining capacity, default '
'behavior is to use all capacity to make a '
'volume')
argparser.add_option('-n', '--name', type='str',
help='Comma separated list of names to use when '
'naming volumes, or selecting a volume for '
'delete. Default behavior is to use '
'implementation provided default names.')
(options, args) = argparser.parse_args()
if len(args) == 1:
args.append('show')
try:
noderange = args[0]
operation = args[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, options, args[2:])
if __name__ == '__main__':
main()
-147
View File
@@ -1,147 +0,0 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.screensqueeze as sq
exitcode = 0
def get_update_progress(session, url):
for res in session.read(url):
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
text = '{0}: {1:3.0f}%'.format(status, percent)
return text, status, detail
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
for node in res.get('databynode', ()):
printerror(res['databynode'][node], node)
def download_servicedata(noderange, media):
global exitcode
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/support/servicedata/'.format(noderange)
upargs = {'filename': filename}
noderrs = {}
nodeurls = {}
for res in session.create(resource, upargs):
if 'created' not in res:
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
'error', 'Unknown Error')
continue
watchurl = res['created']
currnode = watchurl.split('/')[1]
nodeurls[currnode] = '/' + watchurl
while nodeurls:
for node in list(nodeurls):
progress, status, err = get_update_progress(
session, nodeurls[node])
if status == 'error':
exitcode = 1
noderrs[node] = err
if status in ('error', 'complete', 'pending'):
list(session.delete(nodeurls[node]))
del nodeurls[node]
output.set_output(node, progress)
time.sleep(2)
allerrnodes = ','.join(noderrs)
if noderrs:
sys.stderr.write(
'Nodes had errors retrieving service data ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
funmap = {
'servicedata': download_servicedata,
}
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> servicedata "
"<filename>",
epilog='\nservicedata will save service data to the given '
'directory. It is saved to the location on the relevant '
'management server (the confluent server if running remote, '
'and the collective.manager if in collective)\n'
'\n\nSee `man %prog` for more info.\n')
(options, args) = argparser.parse_args()
media = None
try:
noderange = args[0]
operation = args[1]
arglength = 2
if operation == 'servicedata':
media = args[2]
arglength = 3
if len(args) > arglength:
argparser.print_help()
sys.exit(1)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
try:
handler = funmap[operation]
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media)
if __name__ == '__main__':
main()
-1
View File
@@ -1 +0,0 @@
../confluent_server/builddeb
+47 -274
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2018 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,96 +16,16 @@
# limitations under the License.
import anydbm as dbm
import csv
import errno
import fnmatch
import hashlib
import os
import shlex
import socket
import ssl
import sys
import confluent.tlvdata as tlvdata
import confluent.sortutil as sortutil
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
}
class Tabulator(object):
def __init__(self, headers):
self.headers = headers
self.rows = []
def add_row(self, row):
self.rows.append(row)
def get_table(self, order=None):
i = 0
fmtstr = ''
separator = []
for head in self.headers:
if order and order == head:
order = i
neededlen = len(head)
for row in self.rows:
if len(row[i]) > neededlen:
neededlen = len(row[i])
separator.append('-' * (neededlen + 1))
fmtstr += '{{{0}:>{1}}}|'.format(i, neededlen + 1)
i = i + 1
fmtstr = fmtstr[:-1]
yield fmtstr.format(*self.headers)
yield fmtstr.format(*separator)
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
yield fmtstr.format(*row)
else:
for row in self.rows:
yield fmtstr.format(*row)
def write_csv(self, output, order=None):
output = csv.writer(output)
output.writerow(self.headers)
i = 0
for head in self.headers:
if order and order == head:
order = i
i = i + 1
if order is not None:
for row in sorted(
self.rows,
key=lambda x: sortutil.naturalize_string(x[order])):
output.writerow(row)
else:
for row in self.rows:
output.writerow(row)
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:
@@ -123,10 +43,8 @@ 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']
@@ -136,8 +54,6 @@ class Command(object):
self.serverloc = server
if os.path.isabs(self.serverloc) and os.path.exists(self.serverloc):
self._connect_unix()
elif self.serverloc == '/var/run/confluent/api.sock':
raise Exception('Confluent service is not available')
else:
self._connect_tls()
tlvdata.recv(self.connection)
@@ -161,13 +77,8 @@ class Command(object):
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def add_precede_dict(self, dict):
self._prevdict = dict
def handle_results(self, ikey, rc, res, errnodes=None):
def handle_results(self, ikey, rc, res):
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']
@@ -178,8 +89,6 @@ 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]:
@@ -194,19 +103,15 @@ class Command(object):
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
cprint('{0}: {2}->{1}'.format(
print('{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:
cprint('{0}: {1}'.format(node, val))
print('{0}: {1}'.format(node, val))
return rc
def simple_noderange_command(self, noderange, resource, input=None,
key=None, errnodes=None, **kwargs):
key=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
@@ -218,16 +123,15 @@ 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, errnodes)
rc = self.handle_results(ikey, rc, res)
else:
kwargs[ikey] = input
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res, errnodes)
self._currnoderange = None
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
cprint('')
print('')
return 0
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
@@ -251,7 +155,7 @@ class Command(object):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
cprint('')
print('')
return 0
def read(self, path, parameters=None):
@@ -318,7 +222,8 @@ class Command(object):
certreqs = ssl.CERT_NONE
knownhosts = True
self.connection = ssl.wrap_socket(self.connection, ca_certs=cacert,
cert_reqs=certreqs)
cert_reqs=certreqs,
ssl_version=ssl.PROTOCOL_TLSv1)
if knownhosts:
certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
@@ -332,7 +237,7 @@ class Command(object):
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
cprint('Adding new key for %s:%s' % (server, port))
print 'Adding new key for %s:%s' % (server, port)
khf[hostid] = fingerprint
@@ -366,65 +271,42 @@ def attrrequested(attr, attrlist, seenattributes):
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if fnmatch.fnmatch(attr.lower(), candidate.lower()):
if candidate == attr:
seenattributes.add(truename)
return True
elif attr.lower().startswith(candidate.lower() + '.'):
elif attr.startswith(candidate + '.'):
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(path):
for res in session.read('/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in sorted(res['databynode']):
for attr, val in sorted(
res['databynode'][node].items(),
key=lambda (k, v): v.get('sortid', k) if isinstance(v, dict) else k):
if attr == 'error':
sys.stderr.write('{0}: Error: {1}\n'.format(node, val))
continue
if attr == 'errorcode':
exitcode |= val
continue
for node in res['databynode']:
for attr in res['databynode'][node]:
seenattributes.add(attr)
if rename:
printattr = rename.get(attr, attr)
else:
printattr = attr
currattr = res['databynode'][node][attr]
if show_attr(attr, requestargs, seenattributes, options):
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
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, printattr, val)
node, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(node, printattr)
attrout = '{0}: {1}:'.format(node, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node,
printattr)
attrout = '{0}: {1}: ********'.format(node, attr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
attrout = '{0}: {1}:'.format(node, attr)
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
'{2}'.format(node, attr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
@@ -432,14 +314,11 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
dictout = []
for k, v in currattr.items:
dictout.append("{0}={1}".format(k, v))
attrout = '{0}: {1}: {2}'.format(node, printattr, ','.join(map(str, dictout)))
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, dictout)))
else:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or 'broken' in currattr:
print ("CODE ERROR" + repr(attr))
if options.blame or 'broken' in currattr:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
@@ -451,39 +330,7 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
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'])):
cval = ','.join(currattr['value']) if isinstance(
currattr['value'], list) else currattr['value']
dval = ','.join(currattr['default']) if isinstance(
currattr['default'], list) else currattr['default']
cprint('{0}: {1}: {2} (Default: {3})'.format(
node, printattr, cval, dval))
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)
print attrout
if not exitcode:
if requestargs:
for attr in requestargs:
@@ -493,19 +340,6 @@ def print_attrib_path(path, session, requestargs, options, rename=None):
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([])
@@ -543,8 +377,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:
cprint("CODE ERROR" + repr(attr))
cprint(attrout)
print ("CODE ERROR" + repr(attr))
print attrout
if not exitcode:
if requestargs:
for attr in requestargs:
@@ -553,7 +387,7 @@ def printgroupattributes(session, requestargs, showtype, nodetype, noderange, op
exitcode = 1
return exitcode
def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=None):
def updateattrib(session, updateargs, nodetype, noderange, options):
# update attribute
exitcode = 0
if options.clear:
@@ -567,89 +401,28 @@ def updateattrib(session, updateargs, nodetype, noderange, options, dictassign=N
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)
elif dictassign:
for key in dictassign:
if nodetype == 'nodegroups':
exitcode = session.simple_nodegroups_command(
noderange, 'attributes/all', dictassign[key], key)
else:
exitcode = session.simple_noderange_command(
noderange, 'attributes/all', dictassign[key], key)
else:
if "=" in updateargs[1]:
try:
for val in updateargs[1:]:
val = val.split('=', 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]}
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])
else:
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)
exitcode = session.simple_noderange_command(noderange, 'attributes/all'.format(noderange),
val[1], val[0])
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)
@@ -1,87 +0,0 @@
# 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):
if self.nodeoutput[node] == text:
return
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
@@ -1,43 +0,0 @@
# 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)
-222
View File
@@ -1,222 +0,0 @@
# 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, basenode=None):
self.generate_byoutput()
modaloutput = None
ismodal = True
revoutput = []
if basenode:
for checkout in self.byoutput:
if basenode in self.byoutput[checkout]:
modaloutput = checkout
for outdata in sorted(
self.byoutput, key=lambda x: [0 if modaloutput == x else 1,
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()
+6 -30
View File
@@ -20,11 +20,6 @@ from datetime import datetime
import json
import struct
try:
unicode
except NameError:
unicode = str
def decodestr(value):
ret = None
try:
@@ -43,28 +38,17 @@ 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):
_unicode_list(dictdata[key])
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])
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, unicode):
try:
data = data.encode('utf-8')
except AttributeError:
pass
if isinstance(data, str) or isinstance(data, unicode):
if isinstance(data, str):
# plain text, e.g. console data
tl = len(data)
if tl == 0:
@@ -90,14 +74,6 @@ 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)
-280
View File
@@ -2,283 +2,3 @@ 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 nodestorage='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodestorage'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
alias nodelicense='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelicense'
_confluent_get_args()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
CMPARGS+=("")
fi
GENNED=""
for CAND in ${COMP_CANDIDATES[@]}; do
candarray=(${CAND//,/ })
matched=0
for c in "${candarray[@]}"; do
for arg in "${CMPARGS[@]}"; do
if [ "$arg" = "$c" ]; then
matched=1
break
fi
done
done
if [ 0 = $matched ]; then
for c in "${candarray[@]}"; do
GENNED+=" $c"
done
fi
done
}
function _confluent_generic_completion()
{
_confluent_get_args
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeidentify_completion()
{
COMP_CANDIDATES=("on,off -h")
_confluent_generic_completion
}
_confluent_nodesetboot_completion()
{
COMP_CANDIDATES=("default,cd,network,setup,hd -h -b -p")
_confluent_generic_completion
}
_confluent_nodepower_completion()
{
COMP_CANDIDATES=("boot,off,on,status -h -p")
_confluent_generic_completion
}
_confluent_nodemedia_completion()
{
COMP_CANDIDATES=("list,upload,attach,detachall -h")
_confluent_get_args
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[-2]} == 'upload' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -ge 3 ] && [ ! -z "$GENNED" ]; then
COMPREPLY=($(compgen -W "$GENNED" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodefirmware_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "list update" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS -gt 3 ] && [ ${CMPARGS[2]} == 'update' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodeshell_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -c -- ${COMP_WORDS[-1]}))
return
fi
if [ $NUMARGS -gt 3 ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return;
fi
}
_confluent_nodelicense_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "install list" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'install' ]; then
compopt -o default
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
}
_confluent_nodesupport_completion()
{
_confluent_get_args
if [ $NUMARGS == 3 ]; then
COMPREPLY=($(compgen -W "servicedata" -- ${COMP_WORDS[-1]}))
return;
fi
if [ $NUMARGS == 4 ] && [ ${CMPARGS[2]} == 'servicedata' ]; then
compopt -o dirnames
COMPREPLY=()
return
fi
if [ $NUMARGS -lt 3 ]; then
_confluent_nr_completion
return
fi
}
_confluent_nodeattrib_completion()
{
COMP_CANDIDATES=$(nodeattrib '~.>1' all | awk '{print $2}'|sed -e 's/://')
_confluent_generic_completion
}
_confluent_nn_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_nr_completion()
{
CMPARGS=($COMP_LINE)
NUMARGS=${#CMPARGS[@]}
if [ "${COMP_WORDS[-1]}" == '' ]; then
NUMARGS=$((NUMARGS+1))
fi
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
#COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/;nodelist | sed -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
_confluent_ng_completion()
{
_confluent_get_args
if [ $NUMARGS -gt 2 ]; then
return;
fi
INPUT=${COMP_WORDS[-1]}
INPUT=${INPUT##*,-}
INPUT=${INPUT##*,}
INPUT=${INPUT##*@}
PREFIX=""
if [ "$INPUT" != "${COMP_WORDS[-1]}" ]; then
PREFIX=${COMP_WORDS[-1]}
PREFIX=$(echo $PREFIX | sed -e 's/,[^,@-]*$/,/' -e 's/,-[^,@]*$/,-/' -e 's/@[^,@]*/@/')
fi
COMPREPLY=($(compgen -W "$(confetty show /nodegroups|sed -e 's/\///' -e s/^/$PREFIX/)" -- "${COMP_WORDS[-1]}"))
}
complete -F _confluent_nodeattrib_completion nodeattrib
complete -F _confluent_nodeattrib_completion nodegroupattrib
complete -F _confluent_nr_completion nodebmcreset
complete -F _confluent_nodesetboot_completion nodeboot
complete -F _confluent_nr_completion nodeconfig
complete -F _confluent_nn_completion nodeconsole
complete -F _confluent_nr_completion nodeeventlog
complete -F _confluent_nodefirmware_completion nodefirmware
complete -F _confluent_ng_completion nodegroupattrib
complete -F _confluent_ng_completion nodegroupremove
complete -F _confluent_nr_completion nodehealth
complete -F _confluent_nodeidentify_completion nodeidentify
complete -F _confluent_nr_completion nodeinventory
complete -F _confluent_nr_completion nodelist
complete -F _confluent_nodemedia_completion nodemedia
complete -F _confluent_nodepower_completion nodepower
complete -F _confluent_nr_completion noderemove
complete -F _confluent_nr_completion nodereseat
complete -F _confluent_nodeshell_completion noderun
complete -F _confluent_nr_completion nodesensors
complete -F _confluent_nodesetboot_completion nodesetboot
complete -F _confluent_nodeshell_completion nodeshell
complete -F _confluent_nodesupport_completion nodesupport
complete -F _confluent_nodelicense_completion nodelicense
-92
View File
@@ -1,92 +0,0 @@
collate(1) -- Organize text input by node
==============================
## SYNOPSIS
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r] [-l lognametemplate]`
## 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
* `-l`, `--log`:
Save output per node to individual log files, replacing {node} in the name
with the nodename of each
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
* `-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`
-55
View File
@@ -1,55 +0,0 @@
collective(1) -- Check and manage a confluent collective
==============================
## SYNOPSIS
`collective invite <server>`
`collective join <server> [-i TOKEN]`
`collective show`
`collective gencert`
## DESCRIPTION
**collective** helps manage the collective mode of confluent, where multiple
confluent servers are linked together to act as one. For example, the procedure to set up
a collective to run on three servers called mgt1, mgt2, and mgt3, first install and start
confluent as usual on the three servers. On mgt1, run `collective invite mgt2` and an
invitation token will be output. On mgt2, either run `collective join mgt1` to paste
the token interactively, or `collective join mgt1 -i <token>`. At this point, either
mgt1 or mgt2 can bring in mgt3. For example on mgt2 run `collective invite mgt3` and
on mgt3 run `collective join mgt2 -i <token>`
This can be linked together in the following manner with ssh:
on mgt1:
`# ssh mgt2 collective join mgt1 -i $(collective invite mgt2)`
Note that a collective is only redundant with 3 or more members. The collective
will function so long as more than half of the members are online. A collective
of two members is supported, but without redundancy.
Also note that the collective leader role is dynamic, but has no impact on interacting
with confluent. It is merely an internal role that can dynamically change depending
on circumstances.
## OPTIONS
* `-i`:
Provide the token as an argument rather than interactively.
## EXAMPLES
* Inviting a server called mgt2:
`# collective invite mgt2`
`bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
* On mgt2, joining mgt1:
`# collective join mgt1 -i bWd0MkA+BNQ6XAxMXlqJJa+EQRlihL/k9xCXnasgSQXZr989Pa1/ln7G3e1Ncxx6BMzMqqreHJVkPr2FrzjNit/UgHlg`
`Success`
* Showing the collective state:
`# collective show`
`Quorum: True`
`Leader: mgt1`
`Active collective members:`
` mgt2`
+2 -24
View File
@@ -3,11 +3,9 @@ nodeattrib(8) -- List or change confluent nodes attributes
## SYNOPSIS
`nodeattrib [-b] <noderange> [all|<nodeattribute>...]`
`nodeattrib [-b] <noderange> [<nodeattribute>...]`
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -20,19 +18,9 @@ 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 value.
If `-c` is specified, this will set the nodeattribute to a null valid.
This is different from setting the value to an empty string.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
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.
@@ -42,10 +30,6 @@ 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
* `-p`, `--prompt`:
Request interactive prompting to provide values rather than the command line or environment variables.
## EXAMPLES
* Listing matching nodes of a simple noderange:
@@ -91,12 +75,6 @@ See nodegroupattrib(8) command on how to manage attributes on a group level.
`n1: console.method: `
`n2: console.method: `
* List all switches that a node is described as connected to:
`# nodeattrib d1 net.*switch`
`d1: net.mgt.switch: mgtswitch1`
`d1: net.pxe.switch: pxeswitch1`
`d1: net.switch:`
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
@@ -9,18 +9,6 @@ expression to generate the value.
An expression will contain some directives wrapped in `{}` characters. Within
`{}` are a number of potential substitute values and operations.
Note that syntax of expressions can have overlap with the shell syntax.
For example:
`$ echo (n2)`
`-bash: syntax error near unexpected token `n2'`
In such a case, it helps to quote the expression to allow it to be passed:
`$ echo '(n2)'`
`(n2)`
The most common operation is to extract a number from the nodename. These
values are available as n1, n2, etc. So for example attributes for a node named
b1o2r3u4 would have {n1} as 1, {n2} as 2, {n3} as 3, and {n4} as 4.
@@ -51,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={node}-imm`
`hardwaremanagement.manager={nodename}-imm`
Additionally other attributes may be pulled in:
@@ -59,7 +47,7 @@ Additionally other attributes may be pulled in:
Multiple expressions are permissible within a single attribute:
`hardwaremanagement.manager={node}-{hardwaremanagement.method}`
`hardwaremanagement.manager={nodename}-{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
@@ -1,19 +0,0 @@
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`
+2 -34
View File
@@ -4,44 +4,12 @@ nodeboot(8) -- Reboot a confluent node to a specific device
## SYNOPSIS
`nodeboot <noderange>`
`nodeboot [options] <noderange>` [default|cd|network|setup|hd]
`nodeboot <noderange>` [net|setup]
## DESCRIPTION
**nodeboot** reboots nodes in a noderange. If an additional argument is given,
it sets the node to specifically boot to that as the next boot. This
performs an immediate reboot without waiting for the OS. To set the boot
device without inducing a reboot, see the `nodesetboot` command.
## OPTIONS
* `-b`, `--bios`:
For a system that supports both BIOS and UEFI style boot, request BIOS style
boot if supported (some platforms will UEFI boot with this flag anyway).
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `default`:
Request a normal default boot with no particular device override
* `cd`:
Request boot from media. Note that this can include physical CD,
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
key devices.
* `network`:
Request boot to network
* `setup`:
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
* `hd`:
Boot straight to hard disk drive
it sets the node to specifically boot to that as the next boot.
## EXAMPLES
* Booting n3 and n4 to the default boot behavior:
-58
View File
@@ -1,58 +0,0 @@
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.
* `-r`, `--restoredefault`:
Request that the specified component of the targeted nodes will have its
configuration reset to default. Currently the only component implemented
is uefi.
* `-b`, '--batch':
Provide arguments as lines of a file, rather than the command line.
## 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)
+1 -16
View File
@@ -2,7 +2,7 @@ nodeconsole(8) -- Open a console to a confluent node
=====================================================
## SYNOPSIS
`nodeconsole [options] <noderange>`
`nodeconsole <node>`
## DESCRIPTION
@@ -11,17 +11,6 @@ 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.
## OPTIONS
* `-t`, `--tile`:
Use tmux to arrange consoles of the given noderange into a tiled layout on
the terminal screen
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
@@ -53,10 +42,6 @@ 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>`:
-29
View File
@@ -1,29 +0,0 @@
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)
-144
View File
@@ -1,144 +0,0 @@
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
* `-f FIELDS`, `--fields=FIELDS`:
Request a custom set of fields. The available fields are:
Node: The node name if a correlation has been identified
Model: The model number
Serial: The serial number
UUID: The UUID as it should appear in DMI
Type: Device type (e.g. lenovo-xcc, pxe-client, etc)
IP: The confirmed working IP addresses associated with the record
Mac: Mac address of the relevant network interface
Switch: The nearest detected switch to the entry
Port: Port of the switch that most closely connects to the network interface
Advertised IP: IP addresses that may not have been confirmed, but are advertised
* `-o ORDER`, `--order=ORDER`:
Order output by given field. Field names are the same as documented in the -f argument.
* `-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`
+1 -14
View File
@@ -4,23 +4,10 @@ nodefirmware(8) -- Report firmware information on confluent nodes
## SYNOPSIS
`nodefirmware <noderange>`
`nodefirmware <noderange> list|<components>|core`
`nodefirmware <noderange> update [--backup] <filename>`
## DESCRIPTION
`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.
`nodefirmware` reports various pieces of firmware on confluent nodes.
## EXAMPLES
@@ -7,7 +7,6 @@ nodegroupattrib(8) -- List or change confluent nodegroup attributes
`nodegroupattrib <group> [<nodeattribute>...]`
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
`nodeattrib -p <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
@@ -1,24 +0,0 @@
nodegroupdefine(8) -- Define new confluent node group
===================================================================
## SYNOPSIS
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodegroupdefine` allows the definition of a new nodegroup for the confluent management
service. It may only define a single group name at a time.
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
that `nodegroupattrib(8)` will error if a node group does not exist.
## EXAMPLES
* Create a group called `compute`:
`# nodegroupdefine compute`
`compute: created`
## SEE ALSO
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
@@ -1,14 +0,0 @@
nodegrouplist(8) -- List the defined confluent nodegroups
===================================================================
## SYNOPSIS
`nodegrouplist`
## DESCRIPTION
`nodegrouplist` lists the currently defined groups in confluent.
## SEE ALSO
nodeattrib(8), nodeattribexpressions(5), nodegroupattrib(8)
@@ -1,18 +0,0 @@
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`
+2 -17
View File
@@ -3,15 +3,13 @@ nodeinventory(8) -- Get hardware inventory of confluent node
## SYNOPSIS
`nodeinventory <noderange> [serial|model|uuid|mac]`
`nodeinventory <noderange>`
## 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. It accepts
arguments such as serial or model or others as listed above to filter
output to specific data.
as supported by the platforms hardware management implementation.
## EXAMPLES
@@ -106,16 +104,3 @@ output to specific data.
`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`
-11
View File
@@ -1,11 +0,0 @@
nodelicense(8) -- Manage license keys on BMC
=================================================================
## SYNOPSIS
`nodelicense <noderange> [list|install <filename>]`
## DESCRIPTION
`nodelicense` shows and installs license keys on supported BMCs
-3
View File
@@ -17,9 +17,6 @@ displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. There is more
information on node attributes in nodeattributes(5) man page.
Attributes may be specified by wildcard, for example `net.*switch` will report
all attributes that begin with `net.` and end with `switch`.
## OPTIONS
* `-b`, `--blame`:
-57
View File
@@ -1,57 +0,0 @@
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)`
+1 -6
View File
@@ -4,7 +4,7 @@ nodepower(8) -- Check or change power state of confluent nodes
## SYNOPSIS
`nodepower <noderange>`
`nodepower <noderange> [-p] [on|off|boot|shutdown|reset|status]`
`nodepower <noderange> [on|off|boot|shutdown|reset|status]`
## DESCRIPTION
@@ -26,11 +26,6 @@ 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`
-60
View File
@@ -1,60 +0,0 @@
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`
The attribute name may use a wildcard:
`net.*switch=switch1`
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
@@ -1,25 +0,0 @@
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
@@ -1,19 +0,0 @@
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,10 +48,6 @@ 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)
+2 -5
View File
@@ -4,7 +4,7 @@ nodesetboot(8) -- Check or set next boot device for noderange
## SYNOPSIS
`nodesetboot <noderange>`
`nodesetboot [options] <noderange> [default|cd|network|setup|hd]`
`nodesetboot [options] <noderang> [default|cd|network|setup|hd]`
## DESCRIPTION
@@ -30,10 +30,7 @@ control.
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `-u`, `--uefi`:
This flag does nothing, it is for command compatibility with xCAT's rsetboot
* `default`:
Request a normal default boot with no particular device override
+3 -12
View File
@@ -3,19 +3,13 @@ nodeshell(8) -- Execute command on many nodes in a noderange through ssh
## SYNOPSIS
`nodeshell <noderange> [options] <command to execute on each node>`
`nodeshell <noderange> <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). `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.
nodeattribexpressions(5).
## EXAMPLES
@@ -27,10 +21,7 @@ as stderr, unlike psh which combines all stdout and stderr into stdout.
`n4: hi`
* Setting a new static ip address temporarily on secondary interface of four nodes:
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
* If wanting to use literal {} in the command, they must be escaped by doubling:
`# nodeshell n1-n4 "ps | awk '{{print $1}}'"`
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
## SEE ALSO
-65
View File
@@ -1,65 +0,0 @@
nodestorage(8) -- Examine/Modify storage configuration of a node
============================================
## SYNOPSIS
`nodestorage <noderange> [show|create|delete] [options]`
## DESCRIPTION
`nodestorage` provides access to the remote storage configuration of
the noderange.
## OPTIONS
* `-r` **RAIDLEVEL**, `--raidlevel`=**RAIDLEVEL**:
RAID level to use when creating an array
* `-d` **DISKS**, `--disks`=**DISKS**:
Comma separated list of disks to use, or the word "rest" to
indicate use of all available disks
* `-s` **SIZE**, `--size`=**SIZE**:
Comma separated list of sizes to use when creating
volumes. The sizes may be absolute size (e.g. 16gb),
percentage (10%) or the word "rest" to use remaining
capacity, default behavior is to use all capacity to
make a volume
* `-n` **NAME**, `--name`=**NAME**:
Comma separated list of names to use when naming
volumes, or selecting a volume for delete. Default
behavior is to use implementation provided default
## EXAMPLES
* Deleting the volume `somedata`:
`$ nodestorage d5 delete somedata`
`Deleted: somedata`
* Creating a raid5 of 4 disks and a volume named `somedata`:
`$ nodestorage d5 create -r 5 -d drive0,drive_1,drive_2,drive_3 -n somedata
`d5: Volume somedata: Size: 1.905 TB`
`d5: Volume somedata: State: Optimal`
`d5: Volume somedata: Array 1-2`
* Showing current storage configuration of `d3`:
`$ nodestorage d3`
`d3: Disk m.2-0 Description: 128GB M.2 SATA SSD`
`d3: Disk m.2-0 State: online`
`d3: Disk m.2-0 FRU: 00LF428`
`d3: Disk m.2-0 Serial Number: H6B80054`
`d3: Disk m.2-0 Array: 0-0`
`d3: Disk m.2-1 Description: 128GB M.2 SATA SSD`
`d3: Disk m.2-1 State: online`
`d3: Disk m.2-1 FRU: 00LF428`
`d3: Disk m.2-1 Serial Number: H6B80059`
`d3: Disk m.2-1 Array: 0-0`
`d3: Array 0-0 Available Capacity: 0.000 MB`
`d3: Array 0-0 Total Capacity: 131.072 GB`
`d3: Array 0-0 RAID: RAID 1`
`d3: Array 0-0 Disks: m.2-0,m.2-1`
`d3: Array 0-0 Volumes: new_vd`
`d3: Volume new_vd: Size: 122.040 GB`
`d3: Volume new_vd: State: Optimal`
`d3: Volume new_vd: Array 0-0`
-29
View File
@@ -1,29 +0,0 @@
nodesupport(8) -- Utilities for interacting with vendor support
=================================================================
## SYNOPSIS
`nodesupport <noderange> servicedata <directory or filename>`
## DESCRIPTION
`nodesupport` provides capabilities associated with interactiong with support.
Currently it only has the `servicedata` subcommand. `servicedata` takes
an argument that is either a directory name (that can be used for a single node
or multiple nodes) or a file name (only to be used with single node noderange).
Note that the file will be downloaded to the confluent server that actually
connects to the managed system, so it will download to the remote system if running
remotely and will download to the collective.manager indicated system if
running in collective mode.
## EXAMPLES
* Download support data from a single node to a specific filename
`# nodesupport d1 servicedata svcdata.out`
`d1:initializing: 15%`
* Download support data from multiple nodes to a directory
`# nodesupport d1-d4 servicedata service/`
`d1:initializing: 0% d2:initializing: 0% d3:initializing: 0% d4:initializing: 0%`
`# ls service/`
`d1.svcdata d2.svcdata d3.svcdata d4.svcdata`
-2
View File
@@ -1,10 +1,8 @@
#!/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/
@@ -1,35 +0,0 @@
#!/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,7 +4,6 @@ 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:
+17
View File
@@ -0,0 +1,17 @@
[metadata]
name = confluent_common
summary = Confluent Common Libraries
description-file =
README.txt
author = Jarrod Johnson
author-email = jjohnson2@lenovo.com
home-page = http://xcat.sf.net/
classifier =
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
[files]
-2
View File
@@ -1,4 +1,2 @@
include pam/*
include sysvinit/*
include systemd/*
include sysctl/*
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env python
import argparse
import errno
import os
import socket
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.client as client
import confluent.tlvdata as tlvdata
try:
input = raw_input
except NameError:
pass
def make_certificate():
umask = os.umask(0077)
try:
os.makedirs('/etc/confluent/cfg')
except OSError as e:
if e.errno == errno.EEXIST and os.path.isdir('/etc/confluent/cfg'):
pass
else:
raise
if subprocess.check_call(
'openssl ecparam -name secp384r1 -genkey -out '
'/etc/confluent/privkey.pem'.split(' ')):
raise Exception('Error generating private key')
if subprocess.check_call('openssl req -new -x509 -key '
'/etc/confluent/privkey.pem -days 7300 -out '
'/etc/confluent/srvcert.pem -subj /CN='
'{0}'.format(socket.gethostname()).split(' ')):
raise Exception('Error generating certificate')
print('Certificate generated successfully')
os.umask(umask)
def show_invitation(name):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'invite', 'name': name}})
invite = tlvdata.recv(s)['collective']
if 'error' in invite:
sys.stderr.write(invite['error'] + '\n')
return
print('{0}'.format(invite['invitation']))
def join_collective(server, invitation):
if not os.path.exists('/etc/confluent/srvcert.pem'):
make_certificate()
s = client.Command().connection
while not invitation:
invitation = raw_input('Paste the invitation here: ')
tlvdata.send(s, {'collective': {'operation': 'join',
'invitation': invitation,
'server': server}})
res = tlvdata.recv(s)
res = res.get('collective',
{'status': 'Unknown response: ' + repr(res)})
print(res.get('status', res.get('error', repr(res))))
def show_collective():
s = client.Command().connection
tlvdata.send(s, {'collective': {'operation': 'show'}})
res = tlvdata.recv(s)
if 'error' in res['collective']:
print(res['collective']['error'])
return
if 'quorum' in res['collective']:
print('Quorum: {0}'.format(res['collective']['quorum']))
print('Leader: {0}'.format(res['collective']['leader']))
if 'active' in res['collective']:
if res['collective']['active']:
print('Active collective members:')
for member in res['collective']['active']:
print(' {0}'.format(member))
if res['collective']['offline']:
print('Offline collective members:')
for member in res['collective']['offline']:
print(' {0}'.format(member))
else:
print('Run collective show on leader for more data')
def main():
a = argparse.ArgumentParser(description='Confluent server utility')
sp = a.add_subparsers(dest='command')
gc = sp.add_parser('gencert', help='Generate Confluent Certificates for '
'collective mode and remote CLI access')
sl = sp.add_parser('show', help='Show information about the collective')
ic = sp.add_parser('invite', help='Generate a invitation to allow a new '
'confluent instance to join as a '
'collective member. Run collective invite -h for more information')
ic.add_argument('name', help='Name of server to invite to join the '
'collective')
jc = sp.add_parser('join', help='Join a collective. Run collective join -h for more information')
jc.add_argument('server', help='Existing collective member that ran invite and generated a token')
jc.add_argument('-i', help='Invitation provided by runniing invite on an '
'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()
-50
View File
@@ -1,50 +0,0 @@
import shutil
import socket
import subprocess
import tempfile
def get_openssl_conf_location():
# CentOS/RHAT
return '/etc/pki/tls/openssl.cnf'
def get_ip_addresses():
lines = subprocess.check_output('ip addr'.split(' '))
for line in lines.split('\n'):
if line.startswith(' inet6 '):
line = line.replace(' inet6 ', '').split('/')[0]
if line.startswith('fe80::'):
continue
if line == '::1':
continue
elif line.startswith(' inet '):
line = line.replace(' inet ', '').split('/')[0]
if line == '127.0.0.1':
continue
if line.startswith('169.254.'):
continue
else:
continue
yield line
def create_certificate():
shortname = socket.gethostname().split('.')[0]
longname = socket.getfqdn()
subprocess.check_call(
'openssl ecparam -name secp384r1 -genkey -out privkey.pem'.split(' '))
san = ['IP:{0}'.format(x) for x in get_ip_addresses()]
san.append('DNS:{0}'.format(shortname))
san.append('DNS:{0}'.format(longname))
san = ','.join(san)
sslcfg = get_openssl_conf_location()
tmpconfig = tempfile.mktemp()
shutil.copy2(sslcfg, tmpconfig)
with open(tmpconfig, 'a') as cfgfile:
cfgfile.write('\n[SAN]\nsubjectAltName={0}'.format(san))
subprocess.check_call(
'openssl req -new -x509 -key privkey.pem -days 7300 -out cert.pem '
'-subj /CN={0} -extensions SAN '
'-config {1}'.format(longname, tmpconfig).split(' ')
)
if __name__ == '__main__':
create_certificate()
+4 -30
View File
@@ -16,7 +16,6 @@
# limitations under the License.
import getpass
import optparse
import sys
import os
@@ -33,8 +32,6 @@ 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',
@@ -42,14 +39,6 @@ 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()
@@ -62,32 +51,17 @@ if args[0] == 'restore':
if pid is not None:
print("Confluent is running, must shut down to restore db")
sys.exit(1)
try:
cfm.restore_db_from_directory(dumpdir, options.password)
except Exception as e:
print(str(e))
sys.exit(1)
cfm.restore_db_from_directory(dumpdir, options.password)
elif args[0] == 'dump':
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):
if options.password is None and not (options.unprotected or options.redact):
print("Must indicate a password to protect or -u to opt opt of "
"secure value protection or -r to redact sensitive information, "
"or -s to do encrypted backup that requires keys.json from "
"another backup to restore.")
"secure value protection or -r to skip all protected data")
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,
options.skipkeys)
cfm.dump_db_to_directory(dumpdir, options.password, options.redact)
-4
View File
@@ -1,4 +0,0 @@
#!/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)
-63
View File
@@ -1,63 +0,0 @@
#!/bin/bash
cd `dirname $0`
PKGNAME=$(basename $(pwd))
DPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
OPKGNAME=$(basename $(pwd) | sed -e s/_/-/)
if grep wheezy /etc/os-release; then
DPKGNAME=python-$DPKGNAME
fi
cd ..
mkdir -p /tmp/confluent # $DPKGNAME
cp -a * .git /tmp/confluent # $DPKGNAME
cd /tmp/confluent/$PKGNAME
if [ -x ./makeman ]; then
./makeman
fi
./makesetup
VERSION=`cat VERSION`
cat > setup.cfg << EOF
[install]
install-purelib=/opt/confluent/lib/python
install-scripts=/opt/confluent/bin
[sdist_dsc]
package=$DPKGNAME
EOF
python setup.py sdist > /dev/null 2>&1
py2dsc dist/*.tar.gz
shopt -s extglob
cd deb_dist/!(*.orig)/
if [ "$OPKGNAME" = "confluent-server" ]; then
if grep wheezy /etc/os-release; then
sed -i 's/^\(Depends:.*\)/\1, python-confluent-client, python-lxml, python-eficompressor, python-pycryptodomex/' debian/control
else
sed -i 's/^\(Depends:.*\)/\1, confluent-client, python-lxml, python-eficompressor, python-pycryptodome/' debian/control
fi
if grep wheezy /etc/os-release; then
echo 'confluent_client python-confluent-client' >> debian/pydist-overrides
else
echo 'confluent_client confluent-client' >> debian/pydist-overrides
fi
fi
head -n -1 debian/control > debian/control1
mv debian/control1 debian/control
#echo 'Provides: python-'$DPKGNAME >> debian/control
#echo 'Conflicts: python-'$DPKGNAME >> debian/control
#echo 'Replaces: python-'$DPKGNAME' (<<2)' >> debian/control
#echo 'Breaks: python-'$DPKGNAME' (<<2)' >> debian/control
dpkg-buildpackage -rfakeroot -uc -us -i
if [ $? -ne 0 ]; then
echo "[ERROR] rpmbuild returned non-zero, run: rpmbuild -ba ~/rpmbuild/SPECS/$PKGNAME.spec"
exit 1
else
cd -
# Clean up the generated files in this directory
rm -rf $PKGNAME.egg-info dist setup.py
rm -rf $(find deb_dist -mindepth 1 -maxdepth 1 -type d)
if [ ! -z "$1" ]; then
mv deb_dist/* $1/
fi
fi
exit 0
+1 -7
View File
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2018 Lenovo
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -83,8 +83,6 @@ class AsyncSession(object):
self.reaper = eventlet.spawn_after(15, self.destroy)
def add(self, requestid, rsp):
if self.responses is None:
return
self.responses.append((requestid, rsp))
if self._evt:
self._evt.send()
@@ -109,7 +107,6 @@ class AsyncSession(object):
for console in self.consoles:
_consolesessions[console]['session'].destroy()
self.consoles = None
self.responses = None
del _asyncsessions[self.asyncid]
def run_handler(self, handler, requestid):
@@ -169,9 +166,6 @@ def handle_async(env, querydict, threadset):
currsess = AsyncSession()
yield messages.AsyncSession(currsess.asyncid)
return
if querydict['asyncid'] not in _asyncsessions:
raise exc.InvalidArgumentException(
'Invalid or expired async id')
mythreadid = greenlet.getcurrent()
threadset.add(mythreadid)
loggedout = None
+44 -47
View File
@@ -22,7 +22,7 @@
import confluent.config.configmanager as configmanager
import eventlet
import eventlet.tpool
import Cryptodome.Protocol.KDF as KDF
import Crypto.Protocol.KDF as KDF
import hashlib
import hmac
import multiprocessing
@@ -158,13 +158,20 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# but here there's no need for that
eventlet.sleep(0.5)
credobj = Credentials(user, passphrase)
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None:
eventlet.sleep(0.05)
return None
try:
pammy = PAM.pam()
pammy.start(_pamservice, user, credobj.pam_conv)
pammy.authenticate()
pammy.acct_mgmt()
del pammy
return authorize(user, element, tenant, skipuserobj=False)
except NameError:
pass
except PAM.error:
if credobj.haspam:
return None
if (user, tenant) in _passcache:
if hashlib.sha256(passphrase).digest() == _passcache[(user, tenant)]:
if passphrase == _passcache[(user, tenant)]:
return authorize(user, element, tenant)
else:
# In case of someone trying to guess,
@@ -172,48 +179,38 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# invalidate cache and force the slower check
del _passcache[(user, tenant)]
return None
if 'cryptpass' in ucfg:
_passchecking[(user, tenant)] = True
# TODO(jbjohnso): WORKERPOOL
# PBKDF2 is, by design, cpu intensive
# throw it at the worker pool when implemented
# maybe a distinct worker pool, wondering about starving out non-auth stuff
salt, crypt = ucfg['cryptpass']
# execute inside tpool to get greenthreads to give it a special thread
# world
# TODO(jbjohnso): util function to generically offload a call
# such a beast could be passed into pyghmi as a way for pyghmi to
# magically get offload of the crypto functions without having
# to explicitly get into the eventlet tpool game
global authworkers
global authcleaner
if authworkers is None:
authworkers = multiprocessing.Pool(processes=1)
else:
authcleaner.cancel()
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
del _passchecking[(user, tenant)]
eventlet.sleep(
0.05) # either way, we want to stall so that client can't
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None or 'cryptpass' not in ucfg:
eventlet.sleep(0.05) # stall even on test for existence of a username
return None
_passchecking[(user, tenant)] = True
# TODO(jbjohnso): WORKERPOOL
# PBKDF2 is, by design, cpu intensive
# throw it at the worker pool when implemented
# maybe a distinct worker pool, wondering about starving out non-auth stuff
salt, crypt = ucfg['cryptpass']
# execute inside tpool to get greenthreads to give it a special thread
# world
#TODO(jbjohnso): util function to generically offload a call
#such a beast could be passed into pyghmi as a way for pyghmi to
#magically get offload of the crypto functions without having
#to explicitly get into the eventlet tpool game
global authworkers
global authcleaner
if authworkers is None:
authworkers = multiprocessing.Pool(processes=1)
else:
authcleaner.cancel()
authcleaner = eventlet.spawn_after(30, _clean_authworkers)
crypted = eventlet.tpool.execute(_do_pbkdf, passphrase, salt)
del _passchecking[(user, tenant)]
eventlet.sleep(0.05) # either way, we want to stall so that client can't
# determine failure because there is a delay, valid response will
# delay as well
if crypt == crypted:
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant)
try:
pammy = PAM.pam()
pammy.start(_pamservice, user, credobj.pam_conv)
pammy.authenticate()
pammy.acct_mgmt()
del pammy
_passcache[(user, tenant)] = hashlib.sha256(passphrase).digest()
return authorize(user, element, tenant, skipuserobj=False)
except NameError:
pass
except PAM.error:
pass
eventlet.sleep(0.05) # stall even on test for existence of a username
if crypt == crypted:
_passcache[(user, tenant)] = passphrase
return authorize(user, element, tenant)
return None
@@ -1,60 +0,0 @@
# 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
@@ -1,590 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import confluent.collective.invites as invites
import confluent.config.configmanager as cfm
import confluent.exceptions as exc
import confluent.log as log
import confluent.tlvdata as tlvdata
import confluent.util as util
import eventlet
import eventlet.green.socket as socket
import eventlet.green.ssl as ssl
import eventlet.green.threading as threading
import greenlet
import random
try:
import OpenSSL.crypto as crypto
except ImportError:
# while not always required, we use pyopenssl required for at least
# collective
crypto = None
currentleader = None
follower = None
retrythread = None
class ContextBool(object):
def __init__(self):
self.active = False
self.mylock = threading.RLock()
def __enter__(self):
self.active = True
self.mylock.__enter__()
def __exit__(self, exc_type, exc_val, exc_tb):
self.active = False
self.mylock.__exit__(exc_type, exc_val, exc_tb)
connecting = ContextBool()
leader_init = ContextBool()
def connect_to_leader(cert=None, name=None, leader=None):
global currentleader
global follower
if leader is None:
leader = currentleader
log.log({'info': 'Attempting connection to leader {0}'.format(leader),
'subsystem': 'collective'})
try:
remote = connect_to_collective(cert, leader)
except socket.error as e:
log.log({'error': 'Collective connection attempt to {0} failed: {1}'
''.format(leader, str(e)),
'subsystem': 'collective'})
return False
with connecting:
with cfm._initlock:
tlvdata.recv(remote) # the banner
tlvdata.recv(remote) # authpassed... 0..
if name is None:
name = get_myname()
tlvdata.send(remote, {'collective': {'operation': 'connect',
'name': name,
'txcount': cfm._txcount}})
keydata = tlvdata.recv(remote)
if not keydata:
return False
if 'error' in keydata:
if 'backoff' in keydata:
log.log({
'info': 'Collective initialization in progress on '
'{0}'.format(leader),
'subsystem': 'collective'})
return False
if 'leader' in keydata:
log.log(
{'info': 'Prospective leader {0} has redirected this '
'member to {1}'.format(leader, keydata['leader']),
'subsystem': 'collective'})
ldrc = cfm.get_collective_member_by_address(
keydata['leader'])
if ldrc and ldrc['name'] == name:
raise Exception("Redirected to self")
return connect_to_leader(name=name,
leader=keydata['leader'])
if 'txcount' in keydata:
log.log({'info':
'Prospective leader {0} has inferior '
'transaction count, becoming leader'
''.format(leader), 'subsystem': 'collective',
'subsystem': 'collective'})
return become_leader(remote)
return False
follower.kill()
cfm.stop_following()
follower = None
if follower:
follower.kill()
cfm.stop_following()
follower = None
log.log({'info': 'Following leader {0}'.format(leader),
'subsystem': 'collective'})
colldata = tlvdata.recv(remote)
globaldata = tlvdata.recv(remote)
dbi = tlvdata.recv(remote)
dbsize = dbi['dbsize']
dbjson = ''
while (len(dbjson) < dbsize):
ndata = remote.recv(dbsize - len(dbjson))
if not ndata:
try:
remote.close()
except Exception:
pass
raise Exception("Error doing initial DB transfer")
dbjson += ndata
cfm.clear_configuration()
try:
cfm._restore_keys(keydata, None, sync=False)
for c in colldata:
cfm._true_add_collective_member(c, colldata[c]['address'],
colldata[c]['fingerprint'],
sync=False)
for globvar in globaldata:
cfm.set_global(globvar, globaldata[globvar], False)
cfm._txcount = dbi.get('txcount', 0)
cfm.ConfigManager(tenant=None)._load_from_json(dbjson,
sync=False)
cfm.commit_clear()
except Exception:
cfm.stop_following()
cfm.rollback_clear()
raise
currentleader = leader
#spawn this as a thread...
follower = eventlet.spawn(follow_leader, remote)
return True
def follow_leader(remote):
global currentleader
cleanexit = False
try:
cfm.follow_channel(remote)
except greenlet.GreenletExit:
cleanexit = True
finally:
if cleanexit:
log.log({'info': 'Previous following cleanly closed',
'subsystem': 'collective'})
return
log.log({'info': 'Current leader has disappeared, restarting '
'collective membership', 'subsystem': 'collective'})
# The leader has folded, time to startup again...
cfm.stop_following()
currentleader = None
eventlet.spawn_n(start_collective)
def connect_to_collective(cert, member):
remote = socket.create_connection((member, 13001))
# TLS cert validation is custom and will not pass normal CA vetting
# to override completely in the right place requires enormous effort, so just defer until after connect
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE, keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
if cert:
fprint = cert
else:
collent = cfm.get_collective_member_by_address(member)
fprint = collent['fingerprint']
if not util.cert_matches(fprint, remote.getpeercert(binary_form=True)):
# probably Janeway up to something
raise Exception("Certificate mismatch in the collective")
return remote
def get_myname():
try:
with open('/etc/confluent/cfg/myname', 'r') as f:
return f.read().strip()
except IOError:
myname = socket.gethostname()
with open('/etc/confluent/cfg/myname', 'w') as f:
f.write(myname)
return myname
def handle_connection(connection, cert, request, local=False):
global currentleader
global retrythread
operation = request['operation']
if cert:
cert = crypto.dump_certificate(crypto.FILETYPE_ASN1, cert)
else:
if not local:
return
if 'show' == operation:
if not list(cfm.list_collective()):
tlvdata.send(connection,
{'collective': {'error': 'Collective mode not '
'enabled on this '
'system'}})
return
if follower:
linfo = cfm.get_collective_member_by_address(currentleader)
remote = socket.create_connection((currentleader, 13001))
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
cert = remote.getpeercert(binary_form=True)
if not (linfo and util.cert_matches(
linfo['fingerprint'],
cert)):
remote.close()
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
tlvdata.recv(remote) # ignore banner
tlvdata.recv(remote) # ignore authpassed: 0
tlvdata.send(remote,
{'collective': {'operation': 'getinfo',
'name': get_myname()}})
collinfo = tlvdata.recv(remote)
else:
collinfo = {}
populate_collinfo(collinfo)
try:
cfm.check_quorum()
collinfo['quorum'] = True
except exc.DegradedCollective:
collinfo['quorum'] = False
tlvdata.send(connection, {'collective': collinfo})
return
if 'invite' == operation:
try:
cfm.check_quorum()
except exc.DegradedCollective:
tlvdata.send(connection,
{'collective':
{'error': 'Collective does not have quorum'}})
return
#TODO(jjohnson2): Cannot do the invitation if not the head node, the certificate hand-carrying
#can't work in such a case.
name = request['name']
invitation = invites.create_server_invitation(name)
tlvdata.send(connection,
{'collective': {'invitation': invitation}})
connection.close()
if 'join' == operation:
invitation = request['invitation']
try:
invitation = base64.b64decode(invitation)
name, invitation = invitation.split('@', 1)
except Exception:
tlvdata.send(
connection,
{'collective':
{'status': 'Invalid token format'}})
connection.close()
return
host = request['server']
try:
remote = socket.create_connection((host, 13001))
# This isn't what it looks like. We do CERT_NONE to disable
# openssl verification, but then use the invitation as a
# shared secret to validate the certs as part of the join
# operation
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
except Exception:
tlvdata.send(
connection,
{'collective':
{'status': 'Failed to connect to {0}'.format(host)}})
connection.close()
return
mycert = util.get_certificate_from_file(
'/etc/confluent/srvcert.pem')
cert = remote.getpeercert(binary_form=True)
proof = base64.b64encode(invites.create_client_proof(
invitation, mycert, cert))
tlvdata.recv(remote) # ignore banner
tlvdata.recv(remote) # ignore authpassed: 0
tlvdata.send(remote, {'collective': {'operation': 'enroll',
'name': name, 'hmac': proof}})
rsp = tlvdata.recv(remote)
if 'error' in rsp:
tlvdata.send(connection, {'collective':
{'status': rsp['error']}})
connection.close()
return
proof = rsp['collective']['approval']
proof = base64.b64decode(proof)
j = invites.check_server_proof(invitation, mycert, cert, proof)
if not j:
remote.close()
tlvdata.send(connection, {'collective':
{'status': 'Bad server token'}})
connection.close()
return
tlvdata.send(connection, {'collective': {'status': 'Success'}})
connection.close()
currentleader = rsp['collective']['leader']
f = open('/etc/confluent/cfg/myname', 'w')
f.write(name)
f.close()
log.log({'info': 'Connecting to collective due to join',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, rsp['collective'][
'fingerprint'], name)
if 'enroll' == operation:
#TODO(jjohnson2): error appropriately when asked to enroll, but the master is elsewhere
mycert = util.get_certificate_from_file('/etc/confluent/srvcert.pem')
proof = base64.b64decode(request['hmac'])
myrsp = invites.check_client_proof(request['name'], mycert,
cert, proof)
if not myrsp:
tlvdata.send(connection, {'error': 'Invalid token'})
connection.close()
return
myrsp = base64.b64encode(myrsp)
fprint = util.get_fingerprint(cert)
myfprint = util.get_fingerprint(mycert)
cfm.add_collective_member(get_myname(),
connection.getsockname()[0], myfprint)
cfm.add_collective_member(request['name'],
connection.getpeername()[0], fprint)
myleader = get_leader(connection)
ldrfprint = cfm.get_collective_member_by_address(
myleader)['fingerprint']
tlvdata.send(connection,
{'collective': {'approval': myrsp,
'fingerprint': ldrfprint,
'leader': get_leader(connection)}})
if 'assimilate' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not droneinfo:
tlvdata.send(connection,
{'error': 'Unrecognized leader, '
'redo invitation process'})
return
if not util.cert_matches(droneinfo['fingerprint'], cert):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
return
if request['txcount'] < cfm._txcount:
tlvdata.send(connection,
{'error': 'Refusing to be assimilated by inferior'
'transaction count',
'txcount': cfm._txcount,})
return
if connecting.active:
# don't try to connect while actively already trying to connect
tlvdata.send(connection, {'status': 0})
connection.close()
return
if (currentleader == connection.getpeername()[0] and
follower and follower.isAlive()):
# if we are happily following this leader already, don't stir
# the pot
tlvdata.send(connection, {'status': 0})
connection.close()
return
log.log({'info': 'Connecting in response to assimilation',
'subsystem': 'collective'})
eventlet.spawn_n(connect_to_leader, None, None,
leader=connection.getpeername()[0])
tlvdata.send(connection, {'status': 0})
connection.close()
if 'getinfo' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
cert)):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
collinfo = {}
populate_collinfo(collinfo)
tlvdata.send(connection, collinfo)
if 'connect' == operation:
drone = request['name']
droneinfo = cfm.get_collective_member(drone)
if not (droneinfo and util.cert_matches(droneinfo['fingerprint'],
cert)):
tlvdata.send(connection,
{'error': 'Invalid certificate, '
'redo invitation process'})
connection.close()
return
myself = connection.getsockname()[0]
if connecting.active:
tlvdata.send(connection, {'error': 'Connecting right now',
'backoff': True})
connection.close()
return
if myself != get_leader(connection):
tlvdata.send(
connection,
{'error': 'Cannot assimilate, our leader is '
'in another castle', 'leader': currentleader})
connection.close()
return
if request['txcount'] > cfm._txcount:
retire_as_leader()
tlvdata.send(connection,
{'error': 'Client has higher tranasaction count, '
'should assimilate me, connecting..',
'txcount': cfm._txcount})
log.log({'info': 'Connecting to leader due to superior '
'transaction count', 'subsystem': collective})
eventlet.spawn_n(connect_to_leader, None, None,
connection.getpeername()[0])
connection.close()
return
if retrythread:
retrythread.cancel()
retrythread = None
with leader_init:
cfm.update_collective_address(request['name'],
connection.getpeername()[0])
tlvdata.send(connection, cfm._dump_keys(None, False))
tlvdata.send(connection, cfm._cfgstore['collective'])
tlvdata.send(connection, cfm.get_globals())
cfgdata = cfm.ConfigManager(None)._dump_to_json()
tlvdata.send(connection, {'txcount': cfm._txcount,
'dbsize': len(cfgdata)})
connection.sendall(cfgdata)
#tlvdata.send(connection, {'tenants': 0}) # skip the tenants for now,
# so far unused anyway
if not cfm.relay_slaved_requests(drone, connection):
if not retrythread: # start a recovery if everyone else seems
# to have disappeared
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
# ok, we have a connecting member whose certificate checks out
# He needs to bootstrap his configuration and subscribe it to updates
def populate_collinfo(collinfo):
iam = get_myname()
collinfo['leader'] = iam
collinfo['active'] = list(cfm.cfgstreams)
activemembers = set(cfm.cfgstreams)
activemembers.add(iam)
collinfo['offline'] = []
for member in cfm.list_collective():
if member not in activemembers:
collinfo['offline'].append(member)
def try_assimilate(drone):
try:
remote = connect_to_collective(None, drone)
except socket.error:
# Oh well, unable to connect, hopefully the rest will be
# in order
return
tlvdata.send(remote, {'collective': {'operation': 'assimilate',
'name': get_myname(),
'txcount': cfm._txcount}})
tlvdata.recv(remote) # the banner
tlvdata.recv(remote) # authpassed... 0..
answer = tlvdata.recv(remote)
if not answer:
log.log(
{'error':
'No answer from {0} while trying to assimilate'.format(
drone),
'subsystem': 'collective'})
return
if 'txcount' in answer:
log.log({'info': 'Deferring to {0} due to transaction count'.format(
drone), 'subsystem': 'collective'})
connect_to_leader(None, None, leader=remote.getpeername()[0])
return
if 'error' in answer:
log.log({
'error': 'Error encountered while attempting to '
'assimilate {0}: {1}'.format(drone, answer['error']),
'subsystem': 'collective'})
return
log.log({'info': 'Assimilated {0} into collective'.format(drone),
'subsystem': 'collective'})
def get_leader(connection):
if currentleader is None or connection.getpeername()[0] == currentleader:
if currentleader is None:
msg = 'Becoming leader as no leader known'
else:
msg = 'Becoming leader because {0} attempted to connect and it ' \
'is current leader'.format(currentleader)
log.log({'info': msg, 'subsystem': 'collective'})
become_leader(connection)
return currentleader
def retire_as_leader():
global currentleader
cfm.stop_leading()
currentleader = None
def become_leader(connection):
global currentleader
global follower
global retrythread
log.log({'info': 'Becoming leader of collective',
'subsystem': 'collective'})
if follower:
follower.kill()
cfm.stop_following()
follower = None
if retrythread:
retrythread.cancel()
retrythread = None
currentleader = connection.getsockname()[0]
skipaddr = connection.getpeername()[0]
myname = get_myname()
skipem = set(cfm.cfgstreams)
skipem.add(currentleader)
skipem.add(skipaddr)
for member in cfm.list_collective():
dronecandidate = cfm.get_collective_member(member)['address']
if dronecandidate in skipem or member == myname:
continue
eventlet.spawn_n(try_assimilate, dronecandidate)
def startup():
members = list(cfm.list_collective())
if len(members) < 2:
# Not in collective mode, return
return
eventlet.spawn_n(start_collective)
def start_collective():
global follower
global retrythread
if follower:
follower.kill()
cfm.stop_following()
follower = None
try:
if cfm.cfgstreams:
cfm.check_quorum()
# Do not start if we have quorum and are leader
return
except exc.DegradedCollective:
pass
if leader_init.active: # do not start trying to connect if we are
# xmitting data to a follower
return
myname = get_myname()
for member in sorted(list(cfm.list_collective())):
if member == myname:
continue
if cfm.cfgleader is None:
cfm.stop_following(True)
ldrcandidate = cfm.get_collective_member(member)['address']
log.log({'info': 'Performing startup attempt to {0}'.format(
ldrcandidate), 'subsystem': 'collective'})
if connect_to_leader(name=myname, leader=ldrcandidate):
break
else:
retrythread = eventlet.spawn_after(30 + random.random(),
start_collective)
+12 -59
View File
@@ -93,10 +93,9 @@ node = {
'description': ('List of static groups for which this node is '
'considered a member'),
},
'type': {
'description': ('Classification of node as server or switch'),
'validvalues': ('switch', 'server'),
},
#'type': {
# 'description': ('Classification of node as system, vm, etc')
#},
#'id': {
# 'description': ('Numeric identifier for node')
#},
@@ -149,22 +148,6 @@ node = {
# 'autonode.servername, so that would not need to be '
# 'copied ')
# },
'collective.manager': {
'description': ('When in collective mode, the member of the '
'collective currently considered to be responsible '
'for this node. At a future date, this may be '
'modified automatically if another attribute '
'indicates candidate managers, either for '
'high availability or load balancing purposes.')
},
'discovery.passwordrules': {
'description': 'Any specified rules shall be configured on the BMC '
'upon discovery. "expiration=no,loginfailures=no,complexity=no,reuse=no" '
'would disable password expiration, login failures '
'triggering a lockout, password complexity requirements,'
'and any restrictions around reusing an old password.',
'validlistkeys': ('expiration', 'loginfailures', 'complexity', 'reuse'),
},
'discovery.policy': {
'description': 'Policy to use for auto-configuration of discovered '
'and identified nodes. Valid values are "manual", '
@@ -174,7 +157,6 @@ node = {
'so long as the node has no existing public key. '
'"open" allows discovery even if a known public key '
'is already stored',
'validlist': ('manual', 'permissive', 'pxe', 'open'),
},
'info.note': {
'description': 'A field used for administrators to make arbitrary '
@@ -262,13 +244,11 @@ node = {
'console.logging': {
'description': ('Indicate logging level to apply to console. Valid '
'values are currently "full", "interactive", and '
'"none". Defaults to "full".'),
'validvalues': ('full', 'interactive', 'none'),
'"none". Defaults to "full".')
},
'console.method': {
'description': ('Indicate the method used to access the console of '
'the managed node.'),
'validvalues': ('ssh', 'ipmi'),
'the managed node.')
},
# 'virtualization.host': {
# 'description': ('Hypervisor where this node does/should reside'),
@@ -299,17 +279,12 @@ node = {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
},
'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'],
},
'enclosure.bay': {
'description': 'The bay in the enclosure, if any',
# 'appliesto': ['system'],
},
@@ -318,39 +293,17 @@ node = {
#IBM Flex)''',
# 'appliesto': ['system'],
# },
'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.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
+38 -258
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2017-2018 Lenovo
# 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.
@@ -23,18 +23,14 @@
# 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
@@ -142,18 +138,13 @@ def pytechars2line(chars, maxlen=None):
class ConsoleHandler(object):
_plugin_path = '/nodes/{0}/_console/session'
_logtobuffer = True
_genwatchattribs = frozenset(('console.method', 'console.logging',
'collective.manager'))
_genwatchattribs = frozenset(('console.method', 'console.logging'))
def __init__(self, node, configmanager, width=80, height=24):
def __init__(self, node, configmanager):
self.clearpending = False
self.clearerror = False
self.initsize = (width, height)
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'
@@ -199,16 +190,6 @@ 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)
@@ -222,34 +203,16 @@ class ConsoleHandler(object):
def check_isondemand(self):
self._dologging = True
attrvalue = self.cfgmgr.get_node_attributes(
(self.node,), ('console.logging', 'collective.manager'))
(self.node,), ('console.logging',))
if self.node not in attrvalue:
self._isondemand = False
elif 'console.logging' not in attrvalue[self.node]:
self._isondemand = 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
elif (attrvalue[self.node]['console.logging']['value'] not in (
'full', '')):
self._isondemand = True
elif (attrvalue[self.node]['console.logging']['value']) == 'none':
self._dologging = False
def get_buffer_age(self):
"""Return age of buffered data
@@ -261,10 +224,6 @@ 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
@@ -288,7 +247,7 @@ class ConsoleHandler(object):
return
else:
self._ondemand()
if logvalue in ('none', 'memory'):
if logvalue == 'none':
self._dologging = False
if not self._isondemand or self.livesessions:
eventlet.spawn(self._connect)
@@ -309,17 +268,10 @@ class ConsoleHandler(object):
def clearbuffer(self):
self.feedbuffer(
'\x1bc[No data has been received from the remote console since ' \
'connecting. This could\r\nbe due to having the console.logging ' \
'attribute set to none or interactive,\r\nserial console not ' \
'being enabled or incorrectly configured in the OS or\r\nfirmware, ' \
'or the console simply not having any output since last connection]')
'\x1bc[no replay buffer due to console.logging attribute set to '
'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()
@@ -341,8 +293,6 @@ class ConsoleHandler(object):
self._disconnect()
def _connect(self):
if not self._is_local:
return
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread = None
@@ -358,39 +308,22 @@ class ConsoleHandler(object):
self.reconnect.cancel()
self.reconnect = None
try:
self._console = list(plugin.handle_path(
self._console = plugin.handle_path(
self._plugin_path.format(self.node),
"create", self.cfgmgr))[0]
except (exc.NotImplementedException, exc.NotFoundException):
"create", self.cfgmgr)
except exc.NotImplementedException:
self._console = None
except:
if _tracelog:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
else:
print(traceback.format_exc())
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
if not isinstance(self._console, conapi.Console):
self.clearbuffer()
self.connectstate = 'unconnected'
self.error = 'misconfigured'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
self.feedbuffer(
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
'not configured,\r\nset it to a valid value for console '
'function]')
self._send_rcpts(
'\x1bc\x1b[2J\x1b[1;1H[The console.method attribute for this node is '
'not configured,\r\nset it to a valid value for console '
'function]')
self.clearerror = True
return
if self.clearerror:
self.clearerror = False
self.clearbuffer()
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
self.send_break = self._console.send_break
self.resize = self._console.resize
if self._attribwatcher:
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
@@ -402,7 +335,6 @@ class ConsoleHandler(object):
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
try:
self.resize(width=self.initsize[0], height=self.initsize[1])
self._console.connect(self.get_console_output)
except exc.TargetEndpointBadCredentials:
self.clearbuffer()
@@ -410,7 +342,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -420,7 +352,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -432,7 +364,7 @@ class ConsoleHandler(object):
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = self._get_retry_time()
retrytime = 120 + (120 * random.random())
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -440,13 +372,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(
@@ -455,8 +387,6 @@ class ConsoleHandler(object):
self._send_rcpts({'connectstate': self.connectstate})
if self._isalive:
self._connect()
else:
self.clearbuffer()
def close(self):
self._isalive = False
@@ -469,9 +399,6 @@ 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
@@ -541,19 +468,17 @@ class ConsoleHandler(object):
eventdata |= 1
if self.shiftin is not None:
eventdata |= 2
# TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
if self.clearpending or self.clearerror:
self.clearpending = False
self.clearerror = False
self.feedbuffer(b'\x1bc\x1b[2J\x1b[1;1H')
self._send_rcpts(b'\x1bc\x1b[2J\x1b[1;1H')
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
self.feedbuffer(data)
# TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
if self.clearpending:
self.clearpending = False
self.feedbuffer(b'\x1bc')
self._send_rcpts(b'\x1bc')
self._send_rcpts(_utf8_normalize(data, self.shiftin, self.utf8decoder))
def _send_rcpts(self, data):
for rcpt in list(self.livesessions):
@@ -621,23 +546,15 @@ def disconnect_node(node, configmanager):
del _handled_consoles[consk]
def _nodechange(added, deleting, renamed, configmanager):
for node in deleting:
disconnect_node(node, configmanager)
for node in renamed:
disconnect_node(node, configmanager)
connect_node(renamed[node], configmanager)
def _nodechange(added, deleting, configmanager):
for node in added:
connect_node(node, configmanager)
for node in deleting:
disconnect_node(node, configmanager)
def _start_tenant_sessions(cfm):
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
for node in cfm.list_nodes():
try:
connect_node(node, cfm)
except:
@@ -652,130 +569,12 @@ def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager, username=None, direct=True, width=80,
height=24):
attrval = configmanager.get_node_attributes(node, 'collective.manager')
myc = attrval.get(node, {}).get('collective.manager', {}).get(
'value', None)
myname = collective.get_myname()
if myc and myc != collective.get_myname() and direct:
minfo = configmodule.get_collective_member(myc)
return ProxyConsole(node, minfo, myname, configmanager, username,
width, height)
def connect_node(node, configmanager, username=None):
consk = (node, configmanager.tenant)
if consk not in _handled_consoles:
_handled_consoles[consk] = ConsoleHandler(node, configmanager, width,
height)
_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, width,
height):
self.skipreplay = True
self.initsize = (width, height)
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,
'width': self.initsize[0],
'height': self.initsize[1],
#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'})
def resize(self, width, height):
tlvdata.send(self.remote, {'operation': 'resize', 'width': width,
'height': height})
# 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,
@@ -797,9 +596,10 @@ class ConsoleSession(object):
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
connector = connect_node
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False, direct=True, width=80, height=24):
skipreplay=False):
self.registered = False
self.tenant = configmanager.tenant
if not configmanager.is_node(node):
@@ -807,10 +607,6 @@ class ConsoleSession(object):
self.username = username
self.node = node
self.configmanager = configmanager
self.direct = direct # true if client is directly connected versus
# relay
self.width = width
self.height = height
self.connect_session()
self.registered = True
self._evt = None
@@ -839,16 +635,12 @@ class ConsoleSession(object):
between console and shell.
"""
self.conshdl = connect_node(self.node, self.configmanager,
self.username, self.direct, self.width,
self.height)
self.username)
def send_break(self):
"""Send break to remote system
"""
self.conshdl.send_break()
def resize(self, width, height):
self.conshdl.resize(width, height)
def get_buffer_age(self):
"""Get the age in seconds of the buffered data
@@ -874,18 +666,6 @@ 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
+25 -359
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2018 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.
@@ -35,11 +35,7 @@
import confluent
import confluent.alerts as alerts
import confluent.log as log
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
@@ -50,27 +46,11 @@ 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):
@@ -144,7 +124,6 @@ def _init_core():
# be enumerated in any collection
noderesources = {
'attributes': {
'rename': PluginRoute({'handler': 'attributes'}),
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
'expression': PluginRoute({'handler': 'attributes'}),
@@ -167,10 +146,6 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'licenses': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -179,10 +154,6 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'hostname': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'identifier': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -201,39 +172,7 @@ def _init_core():
'default': 'ipmi',
}),
},
},
'storage': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'arrays': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'disks': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'volumes': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
})
},
'system': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'advanced': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'clear': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
})
},
}
},
'_console': {
'session': PluginRoute({
@@ -246,11 +185,6 @@ def _init_core():
'handler': 'ssh',
}),
},
'_enclosure': {
'reseat_bay': PluginRoute(
{'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi'}),
},
'shell': {
# another special case similar to console
'sessions': PluginCollection({
@@ -265,10 +199,6 @@ def _init_core():
'default': 'ipmi',
}),
},
'description': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'events': {
'hardware': {
'log': PluginRoute({
@@ -281,10 +211,6 @@ def _init_core():
}),
},
},
#'forward': {
# # Another dummy value, currently only for the gui
# 'web': None,
#},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
@@ -307,39 +233,13 @@ 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': {
@@ -347,10 +247,6 @@ def _init_core():
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'energy': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
@@ -370,17 +266,10 @@ def _init_core():
},
},
'support': {
'servicedata': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
}
nodegroupresources = {
'attributes': {
'rename': PluginRoute({'handler': 'attributes'}),
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
@@ -437,12 +326,10 @@ def iterate_collections(iterable, forcecollection=True):
def iterate_resources(fancydict):
for resource in fancydict:
for resource in fancydict.iterkeys():
if resource.startswith("_"):
continue
if resource == 'abbreviate':
pass
elif not isinstance(fancydict[resource], PluginRoute): # a resource
if not isinstance(fancydict[resource], PluginRoute): # a resource
resource += '/'
yield msg.ChildCollection(resource)
@@ -476,8 +363,7 @@ 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 nodegroup: {0} not found'.format(nodegroup))
raise exc.NotFoundException("Invalid element requested")
del collectionpath[0:2]
collection = nested_lookup(nodegroupresources, collectionpath)
return iterate_resources(collection)
@@ -497,7 +383,6 @@ 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)
@@ -610,86 +495,8 @@ class BadPlugin(object):
self.node, self.plugin + ' is not a supported plugin')
class BadCollective(object):
def __init__(self, node):
self.node = node
def error(self, *args, **kwargs):
yield msg.ConfluentNodeError(
self.node, 'collective mode is active, but collective.manager '
'is not set for this node')
def abbreviate_noderange(configmanager, inputdata, operation):
if operation != 'create':
raise exc.InvalidArgumentException('Must be a create with nodes in list')
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:
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):
if log.logfull:
raise exc.TargetResourceUnavailable('Filesystem full, free up space and restart confluent service')
iscollection = False
routespec = None
if pathcomponents[0] == 'noderange':
@@ -700,8 +507,6 @@ 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:
@@ -710,8 +515,7 @@ 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 and not (len(pathcomponents) == 3 and
pathcomponents[2] == 'abbreviate'):
if isnoderange:
try:
nodes = noderange.NodeRange(nodeorrange, configmanager).nodes
except Exception as e:
@@ -767,11 +571,10 @@ def handle_node_request(configmanager, inputdata, operation,
else:
raise Exception("TODO here")
del pathcomponents[0:2]
passvalues = queue.Queue()
passvalues = []
plugroute = routespec.routeinfo
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes, isnoderange,
configmanager)
pathcomponents, operation, inputdata, nodes, isnoderange)
if 'handler' in plugroute: # fixed handler definition, easy enough
if isinstance(plugroute['handler'], str):
hfunc = getattr(pluginmap[plugroute['handler']], operation)
@@ -784,37 +587,21 @@ 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'] + ['collective.manager'])
nodes, plugroute['pluginattrs'])
plugpath = None
nodesbymanager = {}
if 'default' in plugroute:
plugpath = plugroute['default']
nodesbyhandler = {}
badcollnodes = []
for node in nodes:
for attrname in plugroute['pluginattrs']:
if attrname in nodeattr[node]:
plugpath = nodeattr[node][attrname]['value']
elif 'default' in plugroute:
plugpath = plugroute['default']
if plugpath in dispatch_plugins:
cfm.check_quorum()
manager = nodeattr[node].get('collective.manager', {}).get(
'value', None)
if manager:
if collective.get_myname() != manager:
if manager not in nodesbymanager:
nodesbymanager[manager] = set([node])
else:
nodesbymanager[manager].add(node)
continue
elif list(cfm.list_collective()):
badcollnodes.append(node)
continue
if plugpath:
if plugpath is not None:
try:
hfunc = getattr(pluginmap[plugpath], operation)
except KeyError:
@@ -824,27 +611,19 @@ def handle_node_request(configmanager, inputdata, operation,
nodesbyhandler[hfunc].append(node)
else:
nodesbyhandler[hfunc] = [node]
for bn in badcollnodes:
nodesbyhandler[BadCollective(bn).error] = [bn]
workers = greenpool.GreenPool()
numworkers = 0
for hfunc in nodesbyhandler:
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})
passvalues.append(hfunc(
nodes=nodesbyhandler[hfunc], element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata))
if isnoderange or not autostrip:
return iterate_queue(numworkers, passvalues)
return itertools.chain(*passvalues)
else:
if numworkers > 0:
return iterate_queue(numworkers, passvalues, nodes[0])
if len(passvalues) > 0:
if isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
else:
raise exc.NotImplementedException()
@@ -854,117 +633,6 @@ def handle_node_request(configmanager, inputdata, operation,
# return stripnode(passvalues[0], nodes[0])
def iterate_queue(numworkers, passvalues, strip=False):
completions = 0
while completions < numworkers:
nv = passvalues.get()
if nv == 'theend':
completions += 1
else:
if isinstance(nv, Exception):
raise nv
if strip and not isinstance(nv, console.Console):
nv.strip_node(strip)
yield nv
def addtoqueue(theq, fun, kwargs):
try:
result = fun(**kwargs)
if isinstance(result, console.Console):
theq.put(result)
else:
for pv in result:
theq.put(pv)
except Exception as e:
theq.put(e)
finally:
theq.put('theend')
def dispatch_request(nodes, manager, element, configmanager, inputdata,
operation):
a = configmanager.get_collective_member(manager)
try:
remote = socket.create_connection((a['address'], 13001))
remote.settimeout(90)
remote = ssl.wrap_socket(remote, cert_reqs=ssl.CERT_NONE,
keyfile='/etc/confluent/privkey.pem',
certfile='/etc/confluent/srvcert.pem')
except Exception:
for node in nodes:
if a:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} is unreachable'.format(
a['name']))
else:
yield msg.ConfluentResourceUnavailable(
node,
'"{0}" is not recognized as a collective member'.format(
manager))
return
if not util.cert_matches(a['fingerprint'], remote.getpeercert(
binary_form=True)):
raise Exception("Invalid certificate on peer")
tlvdata.recv(remote)
tlvdata.recv(remote)
myname = collective.get_myname()
dreq = pickle.dumps({'name': myname, 'nodes': list(nodes),
'path': element,'tenant': configmanager.tenant,
'operation': operation, 'inputdata': inputdata})
tlvdata.send(remote, {'dispatch': {'name': myname, 'length': len(dreq)}})
remote.sendall(dreq)
while True:
try:
rlen = remote.recv(8)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rlen) < 8:
try:
nlen = remote.recv(8 - len(rlen))
except Exception:
nlen = 0
if not nlen:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rlen += nlen
rlen = struct.unpack('!Q', rlen)[0]
if rlen == 0:
break
try:
rsp = remote.recv(rlen)
except Exception:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
while len(rsp) < rlen:
try:
nrsp = remote.recv(rlen - len(rsp))
except Exception:
nrsp = 0
if not nrsp:
for node in nodes:
yield msg.ConfluentResourceUnavailable(
node, 'Collective member {0} went unreachable'.format(
a['name']))
return
rsp += nrsp
rsp = pickle.loads(rsp)
if isinstance(rsp, Exception):
raise rsp
yield rsp
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
if pathcomponents[0] == 'detected':
pass
@@ -1013,8 +681,7 @@ 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,
configmanager=configmanager)
pathcomponents, operation, inputdata)
create_user(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_users(),
forcecollection=False)
@@ -1026,8 +693,7 @@ 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,
configmanager=configmanager)
pathcomponents, operation, inputdata)
update_user(user, inputdata.attribs, configmanager)
return show_user(user, configmanager)
elif pathcomponents[0] == 'events':
File diff suppressed because it is too large Load Diff
@@ -43,9 +43,6 @@ 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, customconfig=None):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
@@ -74,8 +71,6 @@ class NodeHandler(generic.NodeHandler):
ic = self._get_ipmicmd(user, passwd)
else:
raise
if customconfig:
customconfig(ic)
currusers = ic.get_users()
lanchan = ic.get_network_channel()
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
@@ -93,8 +88,7 @@ class NodeHandler(generic.NodeHandler):
if ('secret.hardwaremanagementuser' not in cd or
'secret.hardwaremanagementpassword' not in cd):
raise exc.TargetEndpointBadCredentials(
'secret.hardwaremanagementuser and/or '
'secret.hardwaremanagementpassword was not configured')
'Missing user and/or password')
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
@@ -109,14 +103,9 @@ class NodeHandler(generic.NodeHandler):
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
plen = netconfig['prefix']
newip = '{0}/{1}'.format(newip, plen)
currcfg = ic.get_net_configuration()
if currcfg['ipv4_address'] != newip:
# do not change the ipv4_config if the current config looks
# like it is already accurate
ic.set_net_configuration(ipv4_address=newip,
ipv4_configuration='static',
ipv4_gateway=netconfig[
'ipv4_gateway'])
ic.set_net_configuration(ipv4_address=newip,
ipv4_configuration='static',
ipv4_gateway=netconfig['ipv4_gateway'])
elif self.ipaddr.startswith('fe80::'):
cfg.set_node_attributes(
{nodename: {'hardwaremanagement.manager': self.ipaddr}})
@@ -129,18 +118,16 @@ class NodeHandler(generic.NodeHandler):
if currusers[uid]['name'] == newuser:
# Use existing account that has been created
newuserslot = uid
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
break
else:
newuserslot = lockedusers + 1
if newuserslot < 2:
newuserslot = 2
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
ic.set_user_name(newuserslot, newuser)
ic.set_user_access(newuserslot, lanchan,
privilege_level='administrator')
if newpass != passwd: # don't mess with existing if no change
ic.set_user_password(newuserslot, password=newpass)
# Now to zap others
for uid in currusers:
if uid != newuserslot:
@@ -163,4 +150,4 @@ class NodeHandler(generic.NodeHandler):
raise
if reset:
ic.reset_bmc()
return ic
return
@@ -19,37 +19,21 @@ 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:
if info.get('addresses', False):
targsa = info['addresses'][0]
targsa = info['addresses'][0]
self.ipaddr = targsa[0]
@classmethod
def adequate(cls, info):
# Check if the referenced info is really enough, if false, a rescan
# may occur against the target in a short while
return True
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):
@@ -60,10 +44,9 @@ class NodeHandler(object):
def preconfig(self):
return
def discoverable_by_switch(self, macs):
# Given the number of macs sharing the port, is this handler
# appropriate?
return macs <= self.maxmacs
@property
def discoverable_by_switch(self):
return True
def _savecert(self, certificate):
self._fp = certificate
@@ -15,77 +15,32 @@
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'
@classmethod
def adequate(cls, info):
# We can sometimes receive a partially initialized SLP packet
# This is not adequate for being satisfied
return bool(info.get('attributes', {}))
def 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']).lower()
guiddata['data'])
ipmicmd.oem_init()
bayid = ipmicmd._oem.immhandler.get_property(
'/v2/cmm/sp/7')
if not bayid:
return
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'
#
self.info['enclosure.bay'] = bayid
# enclosure.bay only happens for Flex, nextscale doesn't do it
# this way
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,65 +13,14 @@
# 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 _validate_cert(self, certificate):
# Assumption is by the time we call config, that discovery core has
# vetted self._fp. Our job here then is just to make sure that
# the currect connection matches the previously saved cert
return certificate == self._fp
def set_password_policy(self, ic):
rules = []
for rule in self.ruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
if name.lower() in ('expiry', 'expiration'):
rules.append('passwordDurationDays:' + value)
warndays = '5' if int(value) > 5 else value
rules.append('passwordExpireWarningDays:' + warndays)
if name.lower() in ('lockout', 'loginfailures'):
rules.append('passwordFailAllowdNum:' + value)
if name.lower() == 'reuse':
rules.append('passwordReuseCheckNum:' + value)
if rules:
apirequest = 'set={0}'.format(','.join(rules))
ic.register_key_handler(self._validate_cert)
ic.oem_init()
ic._oem.smmhandler.wc.request('POST', '/data', apirequest)
ic._oem.smmhandler.wc.getresponse().read()
def config(self, nodename):
# SMM for now has to reset to assure configuration applies
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
super(NodeHandler, self).config(nodename)
# notes for smm:
# POST to:
@@ -1,4 +1,4 @@
# Copyright 2017-2019 Lenovo
# 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.
@@ -12,27 +12,41 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.imm as immhandler
import confluent.util as util
import confluent.discovery.handlers.bmc as bmchandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.oem.lenovo.imm as imm
import pyghmi.ipmi.private.util as pygutil
class NodeHandler(immhandler.NodeHandler):
class NodeHandler(bmchandler.NodeHandler):
devname = 'XCC'
@classmethod
def adequate(cls, info):
# We can sometimes receive a partially initialized SLP packet
# This is not adequate for being satisfied
return bool(info.get('attributes', {}))
def 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
@@ -40,8 +54,7 @@ class NodeHandler(immhandler.NodeHandler):
ipmicmd = self._get_ipmicmd()
ipmicmd.xraw_command(netfn=0x3a, command=0xf1, data=(1,))
except pygexc.IpmiException as e:
if (e.ipmicode != 193 and 'Unauthorized name' not in str(e) and
'Incorrect password' not in str(e)):
if e.ipmicode != 193:
# raise an issue if anything other than to be expected
raise
#TODO: decide how to clean out if important
@@ -49,67 +62,6 @@ class NodeHandler(immhandler.NodeHandler):
#if ipmicmd:
# ipmicmd.ipmi_session.logout()
def validate_cert(self, certificate):
# broadly speaking, merely checks consistency moment to moment,
# but if https_cert gets stricter, this check means something
fprint = util.get_fingerprint(self.https_cert)
return util.cert_matches(fprint, certificate)
def set_password_policy(self, ic):
ruleset = {'USER_GlobalMinPassChgInt': '0'}
for rule in self.ruleset.split(','):
if '=' not in rule:
continue
name, value = rule.split('=')
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
if name.lower() in ('expiry', 'expiration'):
ruleset['USER_GlobalPassExpPeriod'] = value
if int(value) < 5:
ruleset['USER_GlobalPassExpWarningPeriod'] = value
if name.lower() in ('lockout', 'loginfailures'):
if value.lower() in ('no', 'none', 'disable', 'disabled'):
value = '0'
ruleset['USER_GlobalMaxLoginFailures'] = value
if name.lower() == 'complexity':
ruleset['USER_GlobalPassComplexRequired'] = value
if name.lower() == 'reuse':
ruleset['USER_GlobalMinPassReuseCycle'] = value
ic.register_key_handler(self.validate_cert)
ic.oem_init()
try:
ic._oem.immhandler.wc.grab_json_response('/api/dataset', ruleset)
except Exception as e:
print(repr(e))
pass
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.
dpp = self.configmanager.get_node_attributes(
nodename, 'discovery.passwordrules')
self.ruleset = dpp.get(nodename, {}).get(
'discovery.passwordrules', {}).get('value', '')
ic = self._bmcconfig(nodename, customconfig=self.set_password_policy)
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
# Ok, we can get the enclosure uuid now..
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]).lower()
lebytes[0], lebytes[1], lebytes[2], bebytes[0], bebytes[1], bebytes[2])
def find_info_in_options(rq, optidx):
@@ -72,7 +72,7 @@ def find_info_in_options(rq, optidx):
return uuid, arch
return uuid, arch
def snoop(handler, protocol=None):
def snoop(handler):
#TODO(jjohnson2): ipv6 socket and multicast for DHCPv6, should that be
#prominent
#TODO(jjohnson2): IP_PKTINFO, recvmsg to get the destination ip, per
@@ -92,7 +92,7 @@ def snoop(handler, protocol=None):
rq = bytearray(rq)
if rq[0] == 1: # Boot request
addrlen = rq[2]
if addrlen > 16 or addrlen == 0:
if addrlen > 16: # max address size in bootp is 16 bytes
continue
netaddr = rq[28:28+addrlen]
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
@@ -16,21 +16,18 @@
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 traceback
import subprocess
_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.
@@ -41,12 +38,6 @@ 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)
@@ -206,7 +197,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(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, idx)
for sa in v6addrs:
try:
net.sendto(data, sa)
@@ -222,13 +213,7 @@ def _find_srvtype(net, net4, srvtype, addresses, xid):
bcast = i4['broadcast']
net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
socket.inet_aton(addr))
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, ('239.255.255.253', 427))
net4.sendto(data, (bcast, 427))
@@ -255,7 +240,7 @@ def _parse_attrlist(attrstr):
currattr = currattr.decode('utf-8')
attribs[currattr] = None
else:
attrname, attrval = currattr.split('=', 1)
attrname, attrval = currattr.split('=')
attrname = attrname.decode('utf-8')
attribs[attrname] = []
for val in attrval.split(','):
@@ -279,7 +264,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] == ',':
@@ -295,11 +280,9 @@ def _parse_attrlist(attrstr):
return attribs
def _parse_attrs(data, parsed, xid=None):
def _parse_attrs(data, parsed):
headinfo = _parse_slp_header(data)
if xid is None:
xid = parsed['xid']
if headinfo['function'] != 7 or headinfo['xid'] != xid:
if headinfo['function'] != 7 or headinfo['xid'] != parsed['xid']:
return
payload = headinfo['payload']
if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
@@ -309,18 +292,8 @@ def _parse_attrs(data, parsed, xid=None):
parsed['attributes'] = _parse_attrlist(attrstr)
def fix_info(info, handler):
if '_attempts' not in info:
info['_attempts'] = 10
if info['_attempts'] == 0:
return
info['_attempts'] -= 1
_add_attributes(info)
handler(info)
def _add_attributes(parsed):
xid = parsed.get('xid', 42)
attrq = _generate_attr_request(parsed['services'][0], xid)
attrq = _generate_attr_request(parsed['services'][0], parsed['xid'])
target = None
# prefer reaching out to an fe80 if present, to be highly robust
# in face of network changes
@@ -335,14 +308,13 @@ def _add_attributes(parsed):
else:
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
net.settimeout(1.0)
net.connect(target)
except socket.error:
return
net.sendall(attrq)
rsp = net.recv(8192)
net.close()
_parse_attrs(rsp, parsed, xid)
_parse_attrs(rsp, parsed)
def query_srvtypes(target):
@@ -364,7 +336,6 @@ def query_srvtypes(target):
while tries and not connected:
tries -= 1
try:
net.settimeout(1.0)
net.connect(target)
connected = True
except socket.error:
@@ -397,7 +368,7 @@ def rescan(handler):
handler(scanned)
def snoop(handler, protocol=None):
def snoop(handler):
"""Watch for SLP activity
handler will be called with a dictionary of relevant attributes
@@ -405,17 +376,16 @@ def snoop(handler, protocol=None):
:param handler:
:return:
"""
tracelog = log.Logger('trace')
active_scan(handler, protocol)
active_scan(handler)
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
net.setsockopt(socket.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(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
mreq = slpg2 + struct.pack('=I', i6idx)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net.setsockopt(socket.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)
@@ -435,62 +405,50 @@ def snoop(handler, protocol=None):
net4.bind(('', 427))
while True:
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:
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)
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)
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
peerbymacaddress[mac]['protocol'] = protocol
handler(peerbymacaddress[mac])
except Exception as e:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
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])
def active_scan(handler, protocol=None):
def active_scan(handler):
known_peers = set([])
for scanned in scan():
for addr in scanned['addresses']:
@@ -501,11 +459,10 @@ def active_scan(handler, protocol=None):
break
known_peers.add(addr)
else:
scanned['protocol'] = protocol
handler(scanned)
def scan(srvtypes=_slp_services, addresses=None, localonly=False):
def scan(srvtypes=_slp_services, addresses=None):
"""Find targets providing matching requested srvtypes
This is a generator that will iterate over respondants to the SrvType
@@ -520,15 +477,13 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
"""
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# increase RCVBUF to max, mitigate chance of
# TODO: 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(IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
net.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
# we are going to do broadcast, so allow that...
initxid = random.randint(0, 32768)
xididx = 0
@@ -547,12 +502,6 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
_grab_rsps((net, net4), rsps, 1, xidmap)
# now to analyze and flesh out the responses
for id in rsps:
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']
@@ -563,4 +512,4 @@ def scan(srvtypes=_slp_services, addresses=None, localonly=False):
if __name__ == '__main__':
def testsnoop(a):
print(repr(a))
snoop(testsnoop)
snoop(testsnoop)

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