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

Compare commits

..

647 Commits

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

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

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

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

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

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

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

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

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

For this, offer suggestion of changing directories.

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

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

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

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

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

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

3) Further limit things to one detected thread per mac address
2017-08-26 22:45:36 -04:00
Jarrod Johnson 2a0b038c30 Merge pull request #83 from aduffy19/noderange
Add noderange man page
2017-08-25 16:42:12 -04:00
Jarrod Johnson deb2c3a94a Correct some minor potential issues and add restart on failure 2017-08-25 16:41:22 -04:00
Amanda Duffy 3f1bb1c151 Add noderange man page 2017-08-25 16:39:07 -04:00
Jarrod Johnson 0e708f4b26 Back off https recheck
With various fixes, it does not make as much sense to be so aggressive
with rechecking https assuming we sense something prior to https
being up.
2017-08-25 09:24:21 -04:00
Jarrod Johnson 5c93976bdb Fix errant certificate handling
Non-replacement detection was being treated as replacement.  Correct
that mistake.
2017-08-24 16:57:06 -04:00
Jarrod Johnson 312af72d9f Fix policy handling if not set 2017-08-24 16:34:57 -04:00
Jarrod Johnson 19f75474fc Fix db restore reliability if replacing existing
If there is an existing DB, there were race conditions in loading the
restore.  Fix by clearing the existing DB once the viability of the
restore dataset has been established
2017-08-24 16:04:04 -04:00
Jarrod Johnson 695e038ced Skip attempt to fixup custom attributes 2017-08-24 16:01:00 -04:00
Jarrod Johnson a98d8dc43c Provide capture of PXE mac addresses to net*.hwaddr
This provides a persistent store of PXE mac addresses for consumption
by other provisioning systems that are generally MAC centric.
2017-08-24 15:42:40 -04:00
Jarrod Johnson 69cbeecf1b Correct fixup for untyped data 2017-08-24 13:44:21 -04:00
Jarrod Johnson 5cd461c6ad Add understanding of net.* attributes to fixup
net.* are special, add them to the fxup so that the bootable attribute
is correctly amended.
2017-08-24 13:26:38 -04:00
Jarrod Johnson de30c53d21 Add net.*.bootable
Provide a flag to indicate that a nic entry is meant to hold
a bootable network interface, for purpose of holding a PXE detected
mac.
2017-08-24 13:15:32 -04:00
Jarrod Johnson a599c346f8 Add support for boolean attributes
Normalize likely textual input to a boolean, but otherwise treat such
data in the same manner as a string.
2017-08-24 13:14:23 -04:00
Jarrod Johnson 1e88ce2d41 Flesh out pxe client info if matching discovery data found
While we cannot get serial number and model number in a PXE request,
if there is a matching uuid, fill in the data from the match.  Also
provide info on the related macs so that info can be correlated in more
complex ways (e.g. which mac is pxe, which mac is BMC, which IP goes
where, etc.)
2017-08-24 10:23:43 -04:00
Jarrod Johnson d337de671e Add missing uuid list generator link
The UUID mapping was not connected fully to the API.
2017-08-23 15:52:20 -04:00
Jarrod Johnson 68c7ab4c90 Add ability to group discovery by uuid
API adds 'by-uuid', which helps group BMC and PXE together to make
relationship clear.
2017-08-23 15:41:16 -04:00
Jarrod Johnson 4af5e2d615 Correct uuid retrieval mistake 2017-08-23 13:17:18 -04:00
Jarrod Johnson 299c5c4e71 Add UUID to returned discovery data when available 2017-08-23 12:57:34 -04:00
Jarrod Johnson e5c330bb1c Be less verbose about non-viable data in periodic recheck
If we have a device that has aged out of usable addresses, do not pester
log about this incessantly.
2017-08-23 12:47:44 -04:00
Jarrod Johnson e1eafa46f9 Be robust in handler init withotu addresses set 2017-08-23 11:16:07 -04:00
Jarrod Johnson 9798f30ef8 Remove codecs getwriter unicode from nodesensors
In nodesensors, things are made into binary strings explicitly,
which causes the codecs.getwriter to choke
2017-08-22 16:54:25 -04:00
Jarrod Johnson d1bafa98bb Merge branch 'master' of github.com:jjohnson42/confluent 2017-08-22 16:50:20 -04:00
Jarrod Johnson 2cdfa8fcae Do not let a single node error stop all pending node
Isolate faults to single node for better robust behavior.
2017-08-22 16:50:04 -04:00
Jarrod Johnson b93a2e60d2 Be more selective in IP pruning
Only prune known bad IPs, not the whole list
2017-08-22 16:47:56 -04:00
Jarrod Johnson e478f8aef3 Add --backup to nodefirmware man page 2017-08-16 17:38:52 -04:00
Jarrod Johnson b35463e594 Implement argument to nodefirmware to backup 2017-08-16 17:33:23 -04:00
Jarrod Johnson 54f25cfa9e Implement access to other banks
Provide backend support for other banks than primary.
2017-08-16 17:30:18 -04:00
Jarrod Johnson 9ff7829373 Fix typo in confetty
Strings do not have a startswitch method...
2017-08-16 15:06:48 -04:00
Jarrod Johnson f3b56cf187 Add abbreviate to man page
abbreviate was not in the man page for collate.
2017-08-16 11:34:36 -04:00
Jarrod Johnson 783837b9be Further collate page formatting fixes 2017-08-16 10:42:44 -04:00
Jarrod Johnson 1b18edcccb Fix formatting issues in collate man page 2017-08-16 10:31:58 -04:00
Jarrod Johnson d972e2c12c Fix man page for section 1 2017-08-16 10:12:03 -04:00
Jarrod Johnson c5cf829117 Avoid stacktrace on pipe on non-ascii
Various commands may (accidentally or intentionally) encounter
non-ascii data.  While python stdout without pipe is fine, when piping
it assumes ascii.  Fix this to always assume utf-8 explicitly.
2017-08-16 09:57:57 -04:00
Jarrod Johnson 2f55a251cd Provide more graceful behavior when input has no ': '
Rather than do a stacktrace, place such data into an 'UNKNOWN' output
group, like xcoll did.
2017-08-16 09:44:22 -04:00
Jarrod Johnson d63f6ec9f0 Fix install of collate man page 2017-08-16 09:42:30 -04:00
Jarrod Johnson 9d582262e6 Update man page information
Document use of collate and firmware update
2017-08-15 16:17:28 -04:00
Jarrod Johnson 2063436ccc Reduce context of collate diff
diff mode is less useful with superfluous context.
Suppress context to focus more on differences.
2017-08-15 16:11:38 -04:00
Jarrod Johnson 6addd05de2 Stop firmware when 'pending', which is another 'completion' 2017-08-14 17:10:42 -04:00
Jarrod Johnson 28259511ed Add abbreviate to collate
Also, perform natural sort on 'nodenames' when not abbreviating
2017-08-14 15:06:57 -04:00
Jarrod Johnson 0393e55eb1 Implement API for reverse noderange
Provide reverse noderange through special
/noderange//abbreviate location.  This should enable
collate to abbreviate noderanges.
2017-08-14 14:41:43 -04:00
Jarrod Johnson f7b964b2ce Implement feedback on final state of firmware
Some things on update are active immediately, others are pending reboot.
Documentation needs to use this to let users know what they need to do
or not need to do after the firmware update.
2017-08-14 10:04:19 -04:00
Jarrod Johnson da0941c229 Suppress noise of sysctl command 2017-08-11 16:37:09 -04:00
Jarrod Johnson 7cec311b22 Fix sysctl packaging info 2017-08-11 16:11:05 -04:00
Jarrod Johnson 8ada0e7cf4 Read in sysctl tune on install
This ensures that the value is ready for slp plugin
2017-08-11 15:45:56 -04:00
Jarrod Johnson 87e2e481c2 Tune the UDP receive buffer
We have a sysctl tune file and option to
go higher so that we get more SLP responses.
2017-08-11 15:43:44 -04:00
Jarrod Johnson 38546514e2 Pull UUID into info for rackmount
UUID was erroneously not pulled.  The block on dense/blade should
only apply to enclosure bay.
2017-08-11 10:07:42 -04:00
Jarrod Johnson 1e54abd29b Implement reverse noderange
This is the library feature.  A remote API call will be addded
so that the likes of collate can use it.
2017-08-10 16:45:38 -04:00
Jarrod Johnson a571faa215 Implement collate command
Also adjust output to be less buffered for sake of '-w' and other
pipe commands.
2017-08-09 17:08:52 -04:00
Jarrod Johnson 87da7b62ae Correct mistake in the model attribute name
The info calls it 'modelnumber' rather than 'model'
2017-08-09 14:55:27 -04:00
Jarrod Johnson 8d02d7cc7f Store serial and model number in id attributes on discovery 2017-08-09 14:31:29 -04:00
Jarrod Johnson 5f1f41b975 Add serial and model number to the id attributes
Lay groundwork for pulling this sort of data in on discovery.  The plan is that *if* serial numbers will
be used as a cue for discovery, it would be in the context of a nodediscover command.
2017-08-08 15:51:27 -04:00
Jarrod Johnson 7c1bbe1618 Fix error in the usage text of nodeinventory 2017-08-08 15:45:53 -04:00
Jarrod Johnson 76e6f2dd17 Update online usage message for nodeinventory 2017-08-08 11:56:06 -04:00
Jarrod Johnson 67f2282cc4 Update nodeinventory page with new usage info 2017-08-08 11:55:31 -04:00
Jarrod Johnson b98eea81ad Add filtering to nodeinventory
Allow filtering output by various parameters.
2017-08-08 11:49:50 -04:00
Jarrod Johnson 3bf84ee1e7 Merge branch 'master' of github.com:jjohnson42/confluent 2017-08-08 10:39:49 -04:00
Jarrod Johnson 498803f70a Fix line wrap in nodebmcreset example
Again, the curious behavior of markdown for line break
to be two spaces at the end bites some documentation.
2017-08-08 08:36:21 -04:00
Jarrod Johnson 9ac7158bdd Fix SMM enablement on XCC in SD530
The form factor is, at least in one test, an array, not a string.  Just in case, tolerate
either way.
2017-08-07 13:46:14 -04:00
Jarrod Johnson 4110d18f43 Fix exitcode init in noderun
The order was incorrect in the code.
2017-08-07 11:42:58 -04:00
Jarrod Johnson efe3db11ce Add trigger for restarting on pyghmi updates
Often times, a pyghmi update carries the substance of
a patch.  Instead of a person having to remember to
manually restart, try to trigger an update to restart
confluent automatically.
2017-08-07 09:51:04 -04:00
Jarrod Johnson c8fbb58ae9 Correct overzealous deletion
Selection error caused if statement to have syntax error.
2017-08-03 09:41:21 -04:00
Jarrod Johnson 05715c6826 Fix noderun in same way as nodeshell
noderun had a similar issue about error handling.
2017-08-03 08:40:58 -04:00
Jarrod Johnson 5d7df6dceb Fix error handling in nodeshell
nodeshell needed to process errors correctly, and not assume databynode would be there.
2017-08-03 08:39:04 -04:00
Jarrod Johnson dbcc33629a Fix nodeshell overeager parsing
In the case of execing to ssh, preserve literal argumants since
ssh is going to parse them.  Pre-parsing will cause ssh to strip out spaces, for example.
2017-08-03 08:33:29 -04:00
Jarrod Johnson 085a9fed37 Do not error if debug or usr1 handling is impossible
Windows is a limited platform, let it run regardless.
2017-08-02 16:43:08 -04:00
Jarrod Johnson 659c4e4217 Avoid trace on deleted node
If a node is deleted, act similar to if it were defined with no console.method, to avoid
superfluous trace output.  In the future, it may make sense to filter out nodes with no
console.method earlier, since a fair amount of startup work is done that is ultimately ignored
for situations where console is not enabled.
2017-08-02 10:00:16 -04:00
Jarrod Johnson 28ba40ccfa Replace SOL locknig with NullLock
Again, eventlet monkeypatching of pyghmi seems to be messing up
use of RLocks...
2017-07-31 10:05:50 -04:00
Jarrod Johnson e2b7440a5c Put id of thread in the trace
When looking at the traces, handy to have ids for each thread for various
purposes.
2017-07-31 10:02:05 -04:00
Jarrod Johnson a55ba02d11 Merge remote-tracking branch 'upstream/master' 2017-07-28 16:05:45 -04:00
Jarrod Johnson 2ef549c2ae Correct the NullLock
NullLock was not what util.protect was expecting, provide more function calls, all of which do nothing.
2017-07-28 16:05:10 -04:00
Jarrod Johnson 8123a77451 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-28 15:53:25 -04:00
Jarrod Johnson 9e44087047 Breakup logentries exceeding 65k
The data length of a log entry must not exceed 65k.  If an attempt is
made to log that much, break it up and duplicate the records.  It may make
sense to indicate a continuation explicitly, but for now just extend.
2017-07-28 15:53:07 -04:00
Jarrod Johnson 6e51e02e1b Merge pull request #81 from aduffy19/nodebmcreset
Nodebmcreset
2017-07-27 15:03:08 -04:00
Amanda Duffy 3e90dd6462 Pass sys.exit() an argument
Change sys.exit() to sys.exit(success)
2017-07-27 14:31:37 -04:00
Jarrod Johnson 4a929eb9b5 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-27 09:03:53 -04:00
Jarrod Johnson 858647bb6d Change to str for exception message
str will tend to present a more normal looking error string.  Use
that so that a user does not have the impression there is a code
issue on expected errors.
2017-07-27 09:03:36 -04:00
Jarrod Johnson 8a416ccc5e Correct mistake in core formatting 2017-07-26 16:57:06 -04:00
Jarrod Johnson 12d0fe21cd Only show error message when errors exist 2017-07-26 16:37:09 -04:00
Jarrod Johnson 7a88a2825d Add 'update' to nodefirmware
nodefirmware can now execute updates on a file by file basis.
2017-07-26 15:54:21 -04:00
Jarrod Johnson bc9a498683 Provide a library for compact text node data
Some processes (e.g. nodefirmware update) warrant a different sort of
strategy to try to abbreviate, where collation falls short.  For this,
provide a columnar output that changes to line by line if stdout is not
a terminal.
2017-07-26 15:09:56 -04:00
Jarrod Johnson b5f016ad50 Provide support for removing update
This allows for cancelling and cleanup after reporting.
2017-07-26 14:10:04 -04:00
Jarrod Johnson eb3f5a8bbc Provide update status through API
Give visibility into the process.
2017-07-26 11:39:27 -04:00
Amanda Duffy 1d0b78267d Add nodebmcreset command and man page 2017-07-25 17:10:52 -04:00
Jarrod Johnson a40f015076 Phase 1 of firmware update support
Provide ability to launch firmware updates.
Next will be ability to enumerate, monitor, and
delete.
2017-07-25 17:01:32 -04:00
Amanda Duffy 4574d1bcc7 Merge remote-tracking branch 'upstream/master' 2017-07-25 15:40:06 -04:00
Jarrod Johnson 0bf21238aa Add bay number to IMM and XCC
The bay number can be opportunisticly grabbed, provide
that info in the discovery api.  In future, should add 'by-bay'
once we have enclosure data as well.
2017-07-25 12:07:18 -04:00
Jarrod Johnson 577456d999 Merge branch 'master' of github.com:jjohnson42/confluent 2017-07-20 11:05:19 -04:00
Jarrod Johnson 1dd40d36a1 Breakup logentries exceeding 65k
The data length of a log entry must not exceed 65k.  If an attempt is
made to log that much, break it up and duplicate the records.  It may make
sense to indicate a continuation explicitly, but for now just extend.
2017-07-20 11:05:06 -04:00
Jarrod Johnson ba9ea1acd8 Treat empty string same as undefined
If an administrator clears the cert fingerprint, they will
likely set it to ''.  In such a case, go down the 'no fingerprint'
path rather than reject it.
2017-07-20 10:20:22 -04:00
Jarrod Johnson 60756d9b41 Fix handling of numeric enclosure.bay
enclosure.bay is integer rather than string now.  Fix the filter
to use format, which is more robust in numeric versus string anyway.
Also, consistently make the underlying data integer rather than
sometimes string.
2017-07-20 10:12:53 -04:00
Jarrod Johnson 2795dfe7b9 Only mod noderange if bad nodes detected
This was triggering a defect in noderange. The
defect should be fixed, but in the meantime,
avoid tripping over it and looking weird anyway.
2017-07-18 15:37:37 -04:00
Jarrod Johnson a922de2ed6 Have nodeboot continue on non-failed nodes
If even one of a noderange failed to complete the
set boot device step, none would progress.  Change
the behavior so that exit code still happens and
processing does stop on timed out nodes, but go
ahead and reboot those that were fine.
2017-07-18 14:17:46 -04:00
Jarrod Johnson 5a2e4a669b Add a library for confluent version of 'xcoll'
Add a diff function with colorized intra-line diff info
2017-06-29 16:58:02 -04:00
Jarrod Johnson c2115f4df9 Fix get/set global with the stateless mode change
Since supporting stateless made the init() optional, the *_global functions
would not work if called first.  Correct this oversight.
2017-06-29 13:08:23 -04:00
Jarrod Johnson fa6650a072 Non-linux fallback for list_interface_indexes()
When errors suggest we are not in linux, make a best
effort using netifaces.
2017-06-29 10:33:10 -04:00
Jarrod Johnson 1116fed1e7 Minor style fix 2017-06-29 10:24:13 -04:00
Jarrod Johnson 0745ab0fdf Auto-encode unicode if needed
If unicode comes in to be crypted and fails, be explicit about utf-8 encoding.
2017-06-29 10:20:24 -04:00
Jarrod Johnson aeb3bd2444 Add a stateless configmanager mode
Certain embodiments of confluent may not
want to persist configuration.  Enable
an opt-in full-volatile config mode.
2017-06-29 10:17:17 -04:00
Jarrod Johnson 5dc2d8c8be Prefer SLP data on probe if available
Avoid attempting login on probe if at all possible.
2017-06-28 16:21:57 -04:00
Jarrod Johnson dc23793d32 Reorder SLP services
Make the most important one likely to be first, so that the
attributes filled out later to fixate on the most relevant.
2017-06-28 16:20:58 -04:00
Jarrod Johnson 3bcf236744 Fix serial numbers with leading spaces
Sometimes the string may have leading spaces, remove those for sane presentation.
2017-06-28 10:13:50 -04:00
Jarrod Johnson ea9caa470b Fix unexpected error on retrieving nodegroup attributes
Detect and more specifically report error message when an unknown group
is used for nodegroup commands.
2017-06-27 15:06:59 -04:00
Jarrod Johnson a894624403 Fix python 2.6 compatibility in log
python 2.6 requires a numeric argument to {}
2017-06-27 14:48:33 -04:00
Jarrod Johnson a3c06a00bf Fix confetty behavior when TERM not set
Particularly in non-interactive ssh, TERM is not set.
Assume empty string if TERM is not set.
2017-06-27 14:04:26 -04:00
Jarrod Johnson 86ed339b48 Add localonly mode to SLP
Sometimes in a likely mismatched IP situation, some SLP things will manage to reply and slow
down.  For now in the case of mismatched IPv4 being likely, provide a mode fixated on link local.
2017-06-26 09:30:23 -04:00
Jarrod Johnson ff6e4d7ab6 Fix up slp.py for windows compatibility
python in Windows does not have a required constant
in socket, hard bake the value if not defined in the module.
2017-06-26 09:23:41 -04:00
Jarrod Johnson 8126ec3791 Implement 'memory' console.logging scheme
Provide way for VT to be maintained without log to file.
2017-06-26 09:18:30 -04:00
Jarrod Johnson 8fab1ce9eb Fix permissions on buildindex.sh 2017-06-26 08:46:22 -04:00
Jarrod Johnson 9078fb01c4 Fix missing data from 1.5.0 merge 2017-06-23 08:45:34 -04:00
Jarrod Johnson e4186841b4 Correct the stub functions to work 2017-06-22 15:50:24 -04:00
Jarrod Johnson 1526a9b92d Add functions to stub out globbing
noderanges use [], and bash can do undesirable things.
Since these commands by and large do not reference files,
or at least files likely to be globbed, disable globbing.
2017-06-22 15:42:30 -04:00
Jarrod Johnson bc1b1a9676 Update nodeshell documentation on stdout/stderr 2017-06-21 14:30:54 -04:00
Jarrod Johnson f4267e6013 Add missing files from the ThinkSystem merge 2017-06-21 14:02:59 -04:00
Jarrod Johnson e0cc67f57a Implement Lenovo Thinksystem and discovery support 2017-06-20 14:56:24 -04:00
Jarrod Johnson 6117a90372 Provide a script to summarize hangtraces files 2017-05-22 09:25:51 -04:00
Jarrod Johnson 5d1423d5ce Merge pull request #80 from duxd2/patch-1
Update MANIFEST.in
2017-05-09 08:38:38 -04:00
michael.du 2055c6d698 Update MANIFEST.in
fix the issue which failed to compile the confluent_client, it is due to the add confluent_env.csh in profile, but not add it in MANIFEST
2017-05-09 13:28:57 +08:00
Jarrod Johnson 44892ae1f9 Merge pull request #79 from ocfplc/csh_profile
Add csh environment
2017-05-03 08:58:41 -04:00
arif 812e34f59b Add csh/tcsh profile in /etc/profile.d 2017-05-03 13:21:36 +01:00
Amanda Duffy 3a53b2c014 Merge remote-tracking branch 'refs/remotes/xcat2/master' 2017-05-02 15:44:56 -04:00
Jarrod Johnson 168f60de06 Merge pull request #78 from aduffy19/power-off-detection
Add power off detection
2017-05-02 15:35:39 -04:00
Amanda Duffy f4c191c280 Fix power off detection 2017-05-02 15:30:39 -04:00
Amanda Duffy 443b678d8a Add power off detection 2017-05-02 14:54:18 -04:00
Jarrod Johnson 2bc7a0c21f Change permissions of nodeshell 2017-04-27 16:49:05 -04:00
Jarrod Johnson e429a703e2 Merge pull request #77 from aduffy19/nodeshell
Nodeshell
2017-04-27 16:45:33 -04:00
Amanda Duffy 342c1fb499 Add nodeshell 2017-04-27 16:44:19 -04:00
Amanda Duffy d79e117c55 Merge remote-tracking branch 'refs/remotes/xcat2/master' 2017-04-27 16:31:45 -04:00
Jarrod Johnson e96b869206 Merge pull request #76 from jjohnson42/reworkworkers
Replace login event mechanism
2017-04-26 14:35:29 -04:00
Jarrod Johnson 68037473d0 Replace login event mechanism
Provide a different scheme that does not involve a wait(), if by chance
the flow dies without getting back to our thread.  wait() has no timeout
so this is a strategy to cope by making sure we hang for no longer than
3 minutes, which is well beyond any time a login should possibly take.
2017-04-26 14:26:00 -04:00
Jarrod Johnson 0e5ac5c683 Merge pull request #75 from jjohnson42/reworkworkers
Add a recourse to drain queue
2017-04-25 16:51:32 -04:00
Jarrod Johnson 6ab4d3c886 Add a recourse to drain queue
While it may not have been possible in eventlet for this to happen,
strictly speaking if it were a thread, it could exit during check for
liveness and leave data on the queue.

To be careful, also drain the queue after all children dead.
2017-04-25 16:48:35 -04:00
Jarrod Johnson 5471aca5b4 Merge pull request #74 from jjohnson42/reworkworkers
Rework IPMI workers
2017-04-25 15:29:14 -04:00
Jarrod Johnson cf97bbe299 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:27:29 -04:00
Jarrod Johnson f91ee5bcdd Merge pull request #73 from alin37/master
Fix nodelist to use correct number of arguments
2017-04-25 13:39:47 -04:00
Allen Lin37 ca25e9fa78 Fix nodelist to use correct number of arguments 2017-04-25 13:38:01 -04:00
Jarrod Johnson b14a53a226 Merge remote-tracking branch 'upstream/master' 2017-04-20 16:26:16 -04:00
Jarrod Johnson b597702146 Merge pull request #71 from alin37/master
Adding nodegroupattrib
2017-04-20 16:06:49 -04:00
Allen Lin37 b64858e0b3 Removing unnecessary case if groups ever has a key of 'value' 2017-04-20 15:41:11 -04:00
Jarrod Johnson 50e85df33c Merge remote-tracking branch 'upstream/master' 2017-04-20 14:59:27 -04:00
Jarrod Johnson f6a0500e4a Merge pull request #72 from aduffy19/nodeidentify
Nodeidentify
2017-04-20 14:33:06 -04:00
Amanda Duffy 27294103cf Complain if not enough args 2017-04-20 14:30:33 -04:00
Amanda Duffy ec90ef889b Merge remote-tracking branch 'refs/remotes/xcat2/master' 2017-04-20 14:19:33 -04:00
Allen Lin37 86e502a47a remove debug and fixing issue checking attribute 2017-04-13 22:07:47 -04:00
Allen Lin37 2042bcb8f0 updating server side for use with nodegroupattrib 2017-04-13 17:36:41 -04:00
Allen Lin 43211cf9a7 Updating file permissions on nodegroupattrib and nodeattrib 2017-04-12 10:50:44 -04:00
Allen Lin37 db15992894 Adding nodegroupattrib to update nodegroups 2017-04-12 10:36:27 -04:00
chenglch d4329ab0f1 Merge pull request #69 from jjohnson42/unbufferedlogs
Change the stdout/stderr/trace logs to not buffer things over time
2017-04-12 10:38:19 +08:00
Jarrod Johnson e230f803ce Correct missing argument
__new__ was missed in the last commit.
2017-04-11 15:42:16 -04:00
Jarrod Johnson 3cae3ed983 Merge remote-tracking branch 'upstream/master' into unbufferedlogs 2017-04-11 14:42:03 -04:00
Jarrod Johnson 2c3127418a Merge remote-tracking branch 'upstream/master' 2017-04-11 14:41:40 -04:00
Jarrod Johnson 563a2b11c3 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:40:05 -04:00
Jarrod Johnson cff2c22fda Merge pull request #68 from alin37/master
refactoring nodelist and nodeattrib to use common code
2017-04-07 09:34:15 -04:00
Allen Lin37 cfd8ac4c75 refactoring nodelist and nodeattrib to use common code 2017-04-06 11:30:08 -04:00
Jarrod Johnson 236d889d5d Fix error on 'isset' data
isset data is to be redacted.  This handles that
situation, and also provides a repr if all else fails.
2017-03-05 09:20:50 -05:00
Jarrod Johnson 919dab9b55 Create auth multiprocessing pool on demand
Most of the time, we don't need this pool.  Create when needed,
and clean up after 30 seconds of inactivity.  This avoids a slow
shutdown that was due to core python hanging in help_finish_stuff,
and as a bonus means most of the time, one only sees one confluent
process, which has been a source of questions already.
2017-03-05 08:28:35 -05:00
Jarrod Johnson eb18796d94 Merge pull request #67 from alin37/master
Fix nodelist and nodeattrib to read attributes in groups
2017-03-03 13:22:14 -05:00
Allen Lin37 fd64a2e68c Fix nodelist and nodeattrib to read attributes in groups 2017-03-03 13:10:29 -05:00
Jarrod Johnson 43b51eec20 Be more friendly about shlex parsing errors
If user provides bad input, be more helpful and
less fatalistic.
2017-03-01 10:31:00 -05:00
Jarrod Johnson 011ba66314 Add a -u option for symmetry with rsetboot
If people want to alias xCAT commands, then this
is required for compatibility.
2017-03-01 10:24:23 -05:00
Jarrod Johnson 49b3ca7381 Merge pull request #66 from alin37/master
Adding nodeattrib
2017-02-28 14:06:38 -05:00
Allen Lin37 16f2a2b1eb Updated for broken expressions 2017-02-28 13:51:36 -05:00
Allen Lin37 f2bbb7c355 Merge branch 'master' of https://github.com/alin37/confluent 2017-02-28 13:43:08 -05:00
Allen Lin37 c78b7fa146 Adding nodeattrib to change and update node attributes 2017-02-28 13:42:39 -05:00
Allen Lin37 1b44d2d781 Setting nodeattrib default to show current 2017-02-28 13:42:39 -05:00
Allen Lin37 2d8004000d Adding nodeattrib to list and update attributes 2017-02-28 13:42:39 -05:00
Allen Lin37 b714cfdf0f Adding nodeattrib to change and update node attributes 2017-02-28 13:27:22 -05:00
Allen Lin37 60a1ba77b7 Setting nodeattrib default to show current 2017-02-17 10:46:57 -05:00
Allen Lin37 bbfed443fc Adding nodeattrib to list and update attributes 2017-02-17 10:44:25 -05:00
Jarrod Johnson f3cfe4ee26 Change strategy for stale data protection
Rather than assuming a global state, have the generator react to
GeneratorExit and clean itself up.
2017-02-15 10:58:08 -05:00
Jarrod Johnson 2dcdfe58c3 Merge remote-tracking branch 'upstream/master' 2017-02-15 10:47:04 -05:00
Jarrod Johnson 2e547129d1 Merge branch 'master' of github.com:jjohnson42/confluent 2017-02-15 10:46:34 -05:00
Jarrod Johnson b39ae42955 Add debug of broken expression to nodelist
If an expression is invalid, have nodelist give the error data
allowing the user to see and potentially take action.
2017-02-15 10:46:27 -05:00
Jarrod Johnson 9e593f0554 Clean up nodepower
nodepower boot and reset output was misleading.
2017-02-15 10:46:27 -05:00
Jarrod Johnson ecc6bcf96c Add the util to the setup.py
Previously packaging was missing the new utility
2017-02-15 10:46:27 -05:00
Jarrod Johnson 6ad383c6ad Assure deterministic enumeration of areas
nodegroup must be restored first.
2017-02-15 10:46:27 -05:00
Jarrod Johnson 446d2270c9 Give a friendlier message on restore of redact DB
A redacted dump will not have a keys.json file, which
is natural.  Replace 'file not found' with a message
indicating the possibility of a redacted dump.
2017-02-15 10:46:27 -05:00
Jarrod Johnson 5395f97a21 Do not write out keys when redacting
It's silly to store keys when redact is requested
2017-02-15 10:46:27 -05:00
Jarrod Johnson 04781e0ece Actually hook up the redact feature 2017-02-15 10:46:27 -05:00
Jarrod Johnson 9bd0b7af9d Make confluentdbutil executable 2017-02-15 10:46:27 -05:00
Jarrod Johnson ae806e55b0 Add a utility to frontend DB dump/restore
This exposes the library functions as a utility
2017-02-15 10:46:26 -05:00
Jarrod Johnson 393ea41696 Implement restore from db
This provides at least restore for the primary tenant (the only tenant currently used by anyone)
2017-02-15 10:46:26 -05:00
Jarrod Johnson b87cb87c2a Flesh out the restoration of the master keys
With this commit, the key portion of import should be complete.
2017-02-15 10:46:26 -05:00
Jarrod Johnson a91d7047b2 Start work on a db restore
Start by parsing the previously dumped key data, since the key data requires special handling.
2017-02-15 10:46:26 -05:00
Jarrod Johnson 638842beec Fix no password backup
If password=None was supplied, it would fail.  Now accomodate unprotected keys.
2017-02-15 10:46:26 -05:00
chenglch 5678942186 Merge pull request #65 from jjohnson42/handleinflight
Protect against stale data in new requests
2017-02-14 15:03:32 +08:00
Jarrod Johnson f46269a6f2 Add debug of broken expression to nodelist
If an expression is invalid, have nodelist give the error data
allowing the user to see and potentially take action.
2017-02-10 16:41:47 -05:00
Jarrod Johnson 583e3474ac Do not yield data to discard 2017-02-10 11:37:56 -05:00
Jarrod Johnson ffbe1ab156 Protect against stale data in new requests
If a caller (reasonably) broke out of a loop, a subsequent call would get old data.
Protect against this by discarding data not consumed if previously called.
2017-02-10 11:20:39 -05:00
Jarrod Johnson 9e4bb84932 Merge branch 'master' into dbbackrestore 2017-02-07 13:02:48 -05:00
Jarrod Johnson ffd1bdfae3 Clean up nodepower
nodepower boot and reset output was misleading.
2017-02-02 17:00:09 -05:00
Jarrod Johnson 02dd29b027 Add the util to the setup.py
Previously packaging was missing the new utility
2017-01-31 15:38:54 -05:00
Jarrod Johnson 3b38d8ac43 Assure deterministic enumeration of areas
nodegroup must be restored first.
2017-01-30 16:38:43 -05:00
Jarrod Johnson c381fefc49 Give a friendlier message on restore of redact DB
A redacted dump will not have a keys.json file, which
is natural.  Replace 'file not found' with a message
indicating the possibility of a redacted dump.
2017-01-30 16:27:49 -05:00
Jarrod Johnson a288136a80 Do not write out keys when redacting
It's silly to store keys when redact is requested
2017-01-30 16:13:49 -05:00
Jarrod Johnson cbc0ffbc1c Actually hook up the redact feature 2017-01-30 16:12:49 -05:00
Jarrod Johnson cfc6fd04fc Make confluentdbutil executable 2017-01-30 16:10:01 -05:00
Jarrod Johnson 4a4b1a623f Add a utility to frontend DB dump/restore
This exposes the library functions as a utility
2017-01-30 16:08:28 -05:00
Jarrod Johnson f1867f900d Implement restore from db
This provides at least restore for the primary tenant (the only tenant currently used by anyone)
2017-01-30 15:31:36 -05:00
Jarrod Johnson 12ed7c50fb Merge branch 'dbbackrestore' of github.com:jjohnson42/confluent into dbbackrestore 2017-01-30 09:10:51 -05:00
Jarrod Johnson 85205867b3 Flesh out the restoration of the master keys
With this commit, the key portion of import should be complete.
2017-01-27 16:50:03 -05:00
Jarrod Johnson 60c1cab56f Merge branch 'master' into dbbackrestore 2017-01-27 16:27:34 -05:00
Jarrod Johnson 15c84e8a9b Modify setup.py.tmpl to be adaptive
This should prevent forgetting to add content to
setup.py moving forward.
2017-01-27 15:23:16 -05:00
Jarrod Johnson 1028f1cb60 Have nodeboot be executable 2017-01-27 14:48:09 -05:00
Jarrod Johnson f245680732 Rename nodeboot.py to nodeboot
No point in having '.py' extensions
2017-01-27 14:29:55 -05:00
Jarrod Johnson 6007a923a9 Merge pull request #63 from aduffy19/master
update power menu
2017-01-27 14:28:45 -05:00
Amanda Duffy 36bc81448e Fix missing ] and add entries to printed menu 2017-01-27 14:25:42 -05:00
Jarrod Johnson 612350ca65 Provide for a terminal session to process normal commands
If it is not one of the 'special' terminal ones, assume it's a normal one.  Recurse into
the process request to handle it as a separate request.
2017-01-27 14:18:22 -05:00
Jarrod Johnson d07e5a08c9 Provide for a terminal session to process normal commands
If it is not one of the 'special' terminal ones, assume it's a normal one.  Recurse into
the process request to handle it as a separate request.
2017-01-27 14:17:29 -05:00
Amanda Duffy 8599485d56 Merge remote-tracking branch 'origin/master' 2017-01-27 13:44:37 -05:00
Amanda Duffy fdf74dbf11 Add power options for console commands 2017-01-27 13:44:12 -05:00
Jarrod Johnson d2e8fa6521 Start work on a db restore
Start by parsing the previously dumped key data, since the key data requires special handling.
2017-01-27 11:20:55 -05:00
Jarrod Johnson eca18a451b Fix no password backup
If password=None was supplied, it would fail.  Now accomodate unprotected keys.
2017-01-27 10:09:15 -05:00
Jarrod Johnson bc5efa8a7e Try restarting confluent if running on update
This mitigates chance of confluent being stale.
2017-01-20 15:50:12 -05:00
Jarrod Johnson 486c322233 Have binding network sockets occur in a retry loop
There seems to be scenarios where a previously used socket won't open up immediately.  Retry when
this is detected.
2017-01-17 13:59:22 -05:00
Jarrod Johnson 548e4404ce Fix confluent startup error if dead
Clean up stale dbg.sock if present rather than fail to start.  Also, if the pid indicated in pid file doesn't exist, ignore
the pidfile contents.
2017-01-06 13:43:02 -05:00
Jarrod Johnson 99e97fe5c4 Add the noderun command to the manifest
The noderun command was not being packaged.  Correct the
oversight so that it will be in packages.
2017-01-06 13:29:25 -05:00
Jarrod Johnson 8c13e738c0 Make usage/help more consistent across the commands
Have every client command run argparse to get a chance at '-h'.  When
lacking arguments, always use print_help() to provide detail rather than usage.
2017-01-06 13:28:28 -05:00
Jarrod Johnson 5ffc2c298b Correct mixup of parentheses and brackets 2017-01-04 09:56:31 -05:00
Jarrod Johnson 7bf8242aba Assign self.ipmicmd in case we are reusing an existing object that is already logged. 2017-01-04 09:45:19 -05:00
Jarrod Johnson b3c28ad33e Defer assigning of self.ipmicmd until login
Wait until logged in to assign.  This way we have confirmation that ipmicmd is viable before trying to use it.
2017-01-04 09:17:07 -05:00
Jarrod Johnson e7bdb5ee7d Defer TLS key handler registration
Wait until the object is logged in before trying to set
the key handler.  It carries some prerequisite on talking
to the BMC, so it is better handled in the logged handler.
2017-01-03 16:00:27 -05:00
Amanda Duffy bdde7e719d Provide a command, nodeboot, to combine setboot and power commands. 2016-12-02 15:06:33 -05:00
Jarrod Johnson 8eef064b9f Provide more useful error on socket error
If a socket error occurred that wasn't -2, the client would get an error
about KeyError, which wouldn't be helpful.  raise the unhandled exception
to provide more informative data in case of an issue.
2016-12-02 10:35:24 -05:00
Jarrod Johnson cb8cd29022 Add two more man page sources 2016-12-01 16:37:50 -05:00
Jarrod Johnson c016c55340 Add HTTP/1.0 cache suppression
There is not a single client that will ever talk
to this service that would not support HTTP/1.1.
However, do this to satisfy a scanner.
2016-11-30 11:42:50 -05:00
Jarrod Johnson d2156f3d67 Move the anti-referrer login to only logout
This has no functional difference, just a cosmetic
difference that does not give the erroneous impression
a logout actually occurred.  This does mean that if a browser
disables cookies and uses the api explorer, there would
be an opportunity for a CSRF.
2016-11-30 10:15:44 -05:00
Jarrod Johnson bf8dff90f3 Provide CSRF protection to logout resource
Though more annoying than harmful, guard against CSRF succeeding
against the logout resource.
2016-11-30 10:04:25 -05:00
Jarrod Johnson 004d40e7ca Implement more CSRF countermeasures
At some expense of convenience, make it difficult
for CSRF to succeed even against the API explorer
for most people.  User/Password will now only be
accepted on hitting enter in the address bar rather
than following any link.
2016-11-30 09:57:58 -05:00
Jarrod Johnson ac084b212e Merge branch 'nodeexec' 2016-11-29 10:50:32 -05:00
Jarrod Johnson 7557136d5d Add a number of security headers
There are a number of headers security scanners expect.
Explicitly declare how strict browser should be with
responses.
2016-11-29 10:49:02 -05:00
Amanda Duffy aeb0b5bb24 Provide a command, nodeboot, to combine setboot and power commands. 2016-11-08 15:15:14 -05:00
Jarrod Johnson 70762d3f6c Undo accidental change to nodesetboot 2016-11-08 14:51:57 -05:00
Jarrod Johnson eeb668bdfa Correct formatting page in nodesensors man page 2016-11-08 14:50:10 -05:00
Jarrod Johnson 61bd415ec4 Make confluent depend on python-dns
Fedora and family naming convention removes
'python' from the 'dnspython' name.  Work
within that standard to improve compatibility
with epel and such.
2016-11-07 14:07:12 -05:00
Jarrod Johnson 107337fdba Merge branch 'master' of github.com:jjohnson42/confluent 2016-11-07 10:19:41 -05:00
Jarrod Johnson eb02247a58 Fix consistency of sessionid
Regardless of whether the client uses it as a session id
or not, the fact remains a sessionid is assigned.
Pass the session id in the auth data even if the client
did not send it.
2016-11-07 10:18:32 -05:00
Jarrod Johnson 53904a2a5a Revert "Remove 'path' condition for session cookie"
This reverts commit 0ad4ae90c9.
2016-11-01 13:45:33 -04:00
Jarrod Johnson 5881ad8b68 Support sub-second interval
If a user requests an interval that is not a whole
number, begin honoring it and adjust the timestamp
precision to indicate milliseconds.  Do not bother
with milliseconds on whole number intervals.
2016-10-25 14:38:31 -04:00
Jarrod Johnson e0211fd8d8 Merge branch 'avoidrecursereplay' 2016-10-25 14:38:04 -04:00
Jarrod Johnson 0ad4ae90c9 Remove 'path' condition for session cookie
The cookie had an unnecessary limitation.  path need not be specified,
and it is possible for a client to request in a way that fails the criteria.
2016-10-21 09:56:14 -04:00
Jarrod Johnson 6f9bdf4a7c Create multiple files on roll conflict
Rather than removing the previous file, append a digit
to denote extra log files sharing the same roll date.
2016-10-18 11:28:47 -04:00
Jarrod Johnson 4eaab9db37 Avoid inifinite replay loop on bad previouslogfile
If circumstances result in a log file after rollover
pointing at itself, break out rather than looping trying
to get the same data from itself.
2016-10-18 11:18:57 -04:00
Jarrod Johnson 92fa2bf4d9 Add a number of security headers
There are a number of headers security scanners expect.
Explicitly declare how strict browser should be with
responses.
2016-10-13 11:08:05 -04:00
Jarrod Johnson 7f9394b33a Handle events without timestamps
If an event is missing a timestamp, omit rather than stack trace
2016-10-11 16:46:01 -04:00
Jarrod Johnson 05a6664165 Add a noderun command
This command enables running various commands against the nodes.
While I was at it, fix permissions on some files in git.
2016-10-06 16:30:29 -04:00
Jarrod Johnson 457f1fe30b Provide resource to allow clients to expand custom expressions
Clients may now format a string as if it were to be an expression for an attribute,
and have the server evaluate it using the same engine without passing through the
attribute engine.  This makes it easier, for example, to do nodeexec n1-n4 ipmitool -H {hardwaremanagement.manager}
2016-10-06 15:51:07 -04:00
Jarrod Johnson 2efadf21b5 Begin writing man pages
Provide a couple of man pages to get started on the effort
to provide man pages for the commands.
2016-10-04 15:10:53 -04:00
Jarrod Johnson bb38ff4588 Fix auto-version build
The build autoversion was not correct.  Use
git describe instead to be more complete.
2016-09-30 13:12:40 -04:00
Jarrod Johnson d4ffc41451 Provide means of asynchttp of passing exceptions
This allows client code to be actually same when
using synchronous versus asynchonous.
2016-09-28 15:00:49 -04:00
Jarrod Johnson fbd5059ade Unconditionally register key handler
For reasons not entirely understood, we can
have an ipmi command object without registering
a key handler.  Unconditionally register the key handler
on each use of the command object.  This still saves the
network load of logging in, so it's not a terrible workaround.
2016-09-28 14:59:43 -04:00
Jarrod Johnson baf8587759 Have the confluent message populate 'errorcode'
This allows web code framework to correctly handle responses
in noderange/async as if they were traditional straightforward rest calls.
2016-09-26 16:00:00 -04:00
Jarrod Johnson 7eb881d7e5 Add API code values to confluent messages
When a message is used that would raise an exception in a singleton
case, make available the appropriate apicode in contexts that would be
relevant (namely multi-node operations and all asynchttp).
2016-09-23 16:44:02 -04:00
Jarrod Johnson 9f0daf324e Refactor error code into exceptions
This makes the exceptions more self describing and simplifies httpapi and sockapi.
An important step to improve asynchttp conveyance of error data that would normally
be an http error in synchronous operation.
2016-09-23 16:32:53 -04:00
Jarrod Johnson ddbc155d6b Merge pull request #61 from aduffy19/fixIndexOutOfRangeError
Add exception check if passvalues is empty
2016-09-23 09:48:35 -04:00
Amanda Duffy 9d86ffee92 Add exception check if passvalues is empty 2016-09-22 16:58:00 -04:00
Jarrod Johnson 1286f8af3c Add facility for Anti-CSRF measures
Enable a WebUI to request an auth token.  This will allow it to indicate it is running in a browser and have the server implement protections such that
other software in the browser cannot send arbitrary requests into the server API.

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

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

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

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

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

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

This change simplifies the relationship, changes away the logging and clientcount counter for
a more robust strategy, and paves the way for the dependent ShellHandler to terminate connected
sessions when the shell session dies.
2016-01-08 11:23:55 -05:00
Jarrod Johnson 849193cf98 Have shell sessions not autoreconnect
Unlike consoles, where the underlying concept is a real
persistent thing that needs some care to reattach to watch,
a shell session should die when it is lost, as a new one would
have to be created anyway.  Modify the disconnect behavior
for a shell session to set closed and notify the receivers.
It should also reap dependent watching objects in a future
change.
2016-01-07 16:51:22 -05:00
Jarrod Johnson 00feca7e5b Have builds include shell plugins
The builds were excluding the ssh plugin, correct the oversight.
2016-01-07 16:15:59 -05:00
Jarrod Johnson be75018609 Instrument sessions/ collection
This causes some additional features into core.  Namely
the ability to use a fixed module rather than a string
defined plugin.  This allows shellserver to implement the
'plugin' interface without living in 'plugins'.  'plugins'
implies modularity and potential eventual choice, but
this functionality is core.  It would make sense for the
'attributes' plugin to be changed to match this strategy.
2016-01-07 15:36:44 -05:00
Jarrod Johnson a891745386 Avoid double connect on shell start
When a shell session is initiated, it registers
a recipient at the same time it would be trying
to establish session for not being a 'wait for
recipient'.  Aggressively mark the state as connecting
to avoid the recipient erroneously thinking things have
not be set into motion yet.  Additionally, have the ssh
plugin avoid a traceback when disconnecting before completing
connection.
2016-01-07 10:30:30 -05:00
Jarrod Johnson 46d3779774 Connect shellserver to httpapi
Have httpapi recognize the difference and start a shellserver
session when appropriate.  Next step will be to wire up enumeration of
current shellserver sessions, debug ssh.py traceback, delete on remote
close, and auto-delete when no client connected after some interval (e.g.
30 minutes).
2016-01-06 17:03:10 -05:00
Jarrod Johnson 5d73548583 Use a plain incrementing session id
UUIDs when a simple number will do are harder to use.
Change to a simple increment id.  This could cause an issue
with multiple management nodes, but I think the sessions
should be contained to the instance used.
2016-01-06 11:40:31 -05:00
Jarrod Johnson 8ae8b79837 Fix misuse of console log for shell sessions
console logging assumptions are not valid for shell sessions.
Correct by modifying the buffer init code to be conditional
and adding a stub 'log' to the ShellHandler class.
2016-01-06 11:27:11 -05:00
Jarrod Johnson 0df21ddeb0 Amend core to pass shell objects through
For 'handler' plugin definitions, also recognize
the 'console' exception to allow the plugin to pass
through to the shellserver infrastructure.
2016-01-06 10:06:09 -05:00
Jarrod Johnson 0e821a7bfe Refactor socket api terminal handler
The 'terminal' handler code is broken out to get ready
for using 'shell' as well as 'console'
2016-01-05 16:29:21 -05:00
Jarrod Johnson 30ed563810 Retrofit consoleserver and make shellserver
Provide a common 'shellserver' capability cloned off of 'consoleserver'.
This will enable the concept of per-user shells with option for multiple
shells per.  Each user will have their own set of shell sessions rather
than shared across users.  Can revisit in future if sharing between
users is desired.
2016-01-05 15:34:52 -05:00
Jarrod Johnson 89edc020d5 Use correct exception params on manual policy
In the rework of the exception to include full certificate
in body, a call was missed in the verify_cert() routine.
2015-12-01 09:11:08 -05:00
Jarrod Johnson e2b79a063d Provide full certificate to client on error
When a certificate error happens, provide full certificate data to the
caller so that they may implement whatever strategy they feel appropriate.
2015-11-30 12:32:14 -05:00
Jarrod Johnson f6ce9f2c1e Add infrastructure for TLS certificate handling
When connecting to peer devices that use TLS, provide
a mechanism of tracking peer fingerprint and handling
missing or mismatch of fingerprint.
2015-11-25 11:39:48 -05:00
Jarrod Johnson dfc1e32546 Merge branch 'sshconsole' of github.com:jjohnson42/confluent into sshconsole 2015-11-03 09:06:59 -05:00
Jarrod Johnson d03ca6eafe Add internal resource for 'shell' 2015-10-28 10:50:27 -04:00
Jarrod Johnson 4d148751e5 Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-28 10:50:22 -04:00
Jarrod Johnson 36dd493021 Merge branch 'sshconsole' of github.com:jjohnson42/confluent into sshconsole 2015-10-21 16:48:54 -04:00
Jarrod Johnson a3e5388303 Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-21 16:48:41 -04:00
Jarrod Johnson 2ff97b93ef Add a module for ssh 'console' API
Create a module that does ssh and treats it like
a console.  The plan is to have a cliserver.py to
behave in a manner resembling consoleserver.py, but
with option to have multiple distinct sessions per
target.
2015-10-21 13:42:31 -04:00
122 changed files with 12271 additions and 996 deletions
+2 -1
View File
@@ -1 +1,2 @@
include confluent_env.sh
include confluent_env.sh
include confluent_env.csh
+86
View File
@@ -0,0 +1,86 @@
#!/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 sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.textgroup as tg
import confluent.client
argparser = optparse.OptionParser(usage="Usage: <other command> | %prog [options]")
argparser.add_option('-a', '--abbreviate', action='store_true',
help='Attempt to use confluent server to shorten noderanges')
argparser.add_option('-d', '--diff', action='store_true',
help='Show what differs between most common '
'output group and others')
argparser.add_option('-w', '--watch', action='store_true',
help='Show intermediate results while running')
argparser.add_option('-s', '--skipcommon', action='store_true',
help='Do not print most common result, only non modal '
'groups, useful when combined with -d')
argparser.add_option('-c', '--count', action='store_true',
help='Also display count of nodes in a given group')
argparser.add_option('-r', '--reverse', action='store_true',
help='Reverse sort order to show biggest output group '
'last')
(options, args) = argparser.parse_args()
if sys.stdin.isatty():
argparser.print_help()
sys.exit(1)
grouped = tg.GroupedData()
if options.abbreviate:
grouped = tg.GroupedData(confluent.client.Command())
else:
grouped = tg.GroupedData()
def print_current():
if options.diff:
grouped.print_deviants(skipmodal=options.skipcommon, count=options.count,
reverse=options.reverse)
else:
grouped.print_all(skipmodal=options.skipcommon,
count=options.count,
reverse=options.reverse)
sys.stdout.flush()
fullline = sys.stdin.readline()
while fullline:
for line in fullline.split('\n'):
if not line:
continue
if ': ' not in line:
line = 'UNKNOWN: ' + line
grouped.add_line(*line.split(': ', 1))
if options.watch:
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
print_current()
fullline = sys.stdin.readline()
if not options.watch:
print_current()
+278 -36
View File
@@ -2,7 +2,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -47,6 +47,7 @@ import optparse
import os
import select
import shlex
import signal
import socket
import sys
import time
@@ -56,7 +57,10 @@ try:
import tty
except ImportError:
pass
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
exitcode = 0
consoleonly = False
consolename = ""
@@ -72,6 +76,7 @@ import confluent.tlvdata as tlvdata
import confluent.client as client
conserversequence = '\x05c' # ctrl-e, c
clearpowermessage = False
oldtcattr = None
fd = sys.stdin
@@ -84,19 +89,57 @@ netserver = None
laststate = {}
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 "
"filesystem. Tab completion is supported to aid in navigation,"
"as is up arrow to recall previous commands and control-r to search"
"previous command history, similar to using bash\n\n"
"The supported commands are:\n"
"cd [location] - Set the current command context, similar to a "
"working directory.\n"
"show [resource] - Present the information about the specified "
"resource, or current context if omitted.\n"
"create [resource] attributename=value attributename=value - Create "
"a new instance of a resource.\n"
"remove [resource] - Remove a resource from a list\n"
"set [resource] attributename=value attributename=value - Change "
"the specified attributes value for the given resource name\n"
"unset [resource] attributename - Clear any value for the given "
"attribute names on a resource.\n"
"start [resource] - When used on a text session resource, it "
"enters remote terminal mode. In this mode, use 'ctrl-e, c, ?' for "
"help"
)
#TODO(jjohnson2): lookup context help for 'target' variable, perhaps
#common with the api document
def updatestatus(stateinfo={}):
global powerstate, powertime, clearpowermessage
status = consolename
info = []
for statekey in stateinfo.iterkeys():
for statekey in stateinfo:
laststate[statekey] = stateinfo[statekey]
if ('connectstate' in laststate and
laststate['connectstate'] != 'connected'):
info.append(laststate['connectstate'])
if laststate['connectstate'] == 'closed':
quitconfetty(fullexit=consoleonly)
if 'error' in laststate:
info.append(laststate['error'])
# error will be repeated if relevant
# 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:
@@ -104,14 +147,14 @@ def updatestatus(stateinfo={}):
if 'showtime' in laststate:
showtime = laststate['showtime']
age = time.time() - laststate['showtime']
if age > 86400: # older than one day
if age > 86400: # older than one day
# disambiguate by putting date in and time
info.append(time.strftime('%m-%dT%H:%M', time.localtime(showtime)))
else:
info.append(time.strftime('%H:%M', time.localtime(showtime)))
if info:
status += ' [' + ','.join(info) + ']'
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;console: %s\x07' % status)
sys.stdout.flush()
@@ -143,7 +186,7 @@ def recurse_format(datum, levels=0):
def prompt():
if os.environ['TERM'] not in ('linux'):
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;confetty: %s\x07' % target)
try:
return raw_input(target + ' -> ')
@@ -167,6 +210,7 @@ valid_commands = [
'remove',
'rm',
'delete',
'help',
]
candidates = None
@@ -233,7 +277,11 @@ def rcompleter(text, state):
def parse_command(command):
args = shlex.split(command, posix=True)
try:
args = shlex.split(command, posix=True)
except ValueError as ve:
print('Error: ' + str(ve))
return []
return args
@@ -247,14 +295,17 @@ def print_result(res):
if 'databynode' in res:
print_result(res['databynode'])
return
for key in res.iterkeys():
for key in sorted(res):
notes = []
if res[key] is None:
attrstr = '%s=""' % key
elif type(res[key]) == list:
attrstr = '%s=%s' % (key, recurse_format(res[key]))
elif not isinstance(res[key], dict):
print '{0}: {1}'.format(key, res[key])
try:
print '{0}: {1}'.format(key, res[key])
except UnicodeEncodeError:
print '{0}: {1}'.format(key, repr(res[key]))
continue
elif 'value' in res[key] and res[key]['value'] is not None:
attrstr = '%s="%s"' % (key, res[key]['value'])
@@ -300,22 +351,67 @@ def do_command(command, server):
return
argv[0] = argv[0].lower()
if argv[0] == 'exit':
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(0)
elif argv[0] in ('help', '?'):
return print_help()
elif argv[0] == 'cd':
otarget = target
if len(argv) > 1:
target = fullpath_target(argv[1], forcepath=True)
else: # cd by itself, go 'home'
target = '/'
for res in session.read(target, server):
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
if target[-1] == '/':
parentpath = target[:-1]
else:
parentpath = target
if parentpath:
childname = '{0}/'.format(parentpath[parentpath.rindex('/') + 1:])
parentpath = parentpath[:parentpath.rindex('/') + 1]
if parentpath == '/noderange/':
for res in session.read(target, server):
if 'errorcode' in res:
exitcode = res['errorcode']
target = otarget
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
else:
foundchild = False
for res in session.read(parentpath, server):
try:
if res['item']['href'] == childname:
foundchild = True
except KeyError:
pass
if 'errorcode' in res:
exitcode = res['errorcode']
target = otarget
if 'error' in res:
sys.stderr.write(target + ': ' + res['error'] + '\n')
target = otarget
if not foundchild:
sys.stderr.write(target + ': Target not found - \n')
target = otarget
elif argv[0] in ('cat', 'show', 'ls', 'dir'):
if len(argv) > 1:
targpath = fullpath_target(argv[1])
if argv[0] in ('ls', 'dir'):
if targpath[-1] != '/':
# could still be a directory, fetch the parent..
childname = targpath[targpath.rindex('/') + 1:]
parentpath = targpath[:targpath.rindex('/') + 1]
if parentpath != '/noderange/':
# if it were /noderange/, then it's a directory
# even though parent won't tell us that
for res in session.read(parentpath, server):
try:
if res['item']['href'] == childname:
print(childname)
return
except KeyError:
pass
else:
targpath = target
for res in session.read(targpath):
@@ -378,14 +474,21 @@ def createresource(args):
if keydata is None:
return
targpath = fullpath_target(resname)
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
if targpath.startswith('/noderange//'):
collection = targpath
else:
collection, _, resname = targpath.rpartition('/')
keydata['name'] = resname
makecall(session.create, (collection, keydata))
def makecall(callout, args):
global exitcode
for response in callout(*args):
if 'deleted' in response:
print("Deleted: " + response['deleted'])
if 'created' in response:
print("Created: " + response['created'])
if 'error' in response:
if 'errorcode' in response:
exitcode = response['errorcode']
@@ -494,7 +597,11 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, currfl ^ os.O_NONBLOCK)
if oldtcattr is not None:
termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, oldtcattr)
# Request default color scheme, to undo potential weirdness of terminal
sys.stdout.write('\x1b[m')
if fullexit:
if os.environ.get('TERM', '') not in ('linux'):
sys.stdout.write('\x1b]0;\x07')
sys.exit(code)
else:
tlvdata.send(session.connection, {'operation': 'stop',
@@ -514,7 +621,7 @@ def get_session_node(shellargs):
return None
def conserver_command(filehandle, command):
def conserver_command(filehandle, localcommand):
# x - conserver has that as 'show baud', I am inclined to replace that with
# 'request exclusive'
# b - conserver has that as 'broadcast message', I'm tempted to use that
@@ -529,33 +636,139 @@ def conserver_command(filehandle, command):
# d - down a console... never used this...
# L - toggle logging
# w - who is on console
while not command:
ready, _, _ = select.select((filehandle,), (), (), 1)
if ready:
command += filehandle.read()
if command[0] == '.':
cmdlen = 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[0] == '.':
print("disconnect]\r")
quitconfetty(fullexit=consoleonly)
elif command[0] == 'o':
elif localcommand[0] == 'o':
tlvdata.send(session.connection, {'operation': 'reopen',
'path': currconsole})
print('reopen]\r')
elif command[0] == 'b':
elif localcommand[0] == 'b':
tlvdata.send(session.connection, {'operation': 'break',
'path': currconsole})
print("break sent]\r")
elif command[0] == '?':
elif localcommand[0] == 'p': # power
cmdlen += 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[1] == 'o': # off
print("powering off...")
session.simple_noderange_command(consolename, '/power/state', 'off')
print("complete]\r")
elif localcommand[1] == 's': # shutdown
print("shutting down...")
session.simple_noderange_command(consolename, '/power/state', 'shutdown')
print("complete]\r")
elif localcommand[1] == 'b': # boot
cmdlen += 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[2] == 's': # boot to setup
print("booting to setup...")
bootmode = 'uefi'
bootdev = 'setup'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == 'n': # boot to network
print("booting to network...")
bootmode = 'uefi'
bootdev = 'network'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == '\x0d': # boot to default
print("booting to default...")
bootmode = 'uefi'
bootdev = 'default'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
else:
print("Unknown boot state.]\r")
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)
elif localcommand[0] == '?':
print("help]\r")
print(". disconnect\r")
print("b break\r")
print("o reopen\r")
print(". exit console\r")
print("b break\r")
print("o reopen\r")
print("po power off\r")
print("ps shutdown\r")
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 command[0] == '\x0d':
elif localcommand[0] == '\x0d':
print("ignored]\r")
else: # not a command at all..
print("unknown -- use '?']\r")
def get_command_bytes(filehandle, localcommand, cmdlen):
while len(localcommand) < cmdlen:
ready, _, _ = select.select((filehandle,), (), (), 1)
if ready:
localcommand += filehandle.read()
return localcommand
def check_escape_seq(currinput, filehandle):
while conserversequence.startswith(currinput):
if currinput.startswith(conserversequence): # We have full sequence
@@ -622,6 +835,8 @@ 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
@@ -637,10 +852,22 @@ elif shellargs:
do_command(command, netserver)
quitconfetty(fullexit=True, fixterm=False)
powerstate = None
powertime = None
def check_power_state():
tlvdata.send(
session.connection,
{'operation': 'retrieve',
'path': '/nodes/' + consolename + '/power/state'})
return
while inconsole or not doexit:
if inconsole:
rdylist, _, _ = select.select(
(sys.stdin, session.connection), (), (), 60)
(sys.stdin, session.connection), (), (), 10)
for fh in rdylist:
if fh == session.connection:
# this only should get called in the
@@ -656,7 +883,15 @@ while inconsole or not doexit:
updatestatus(data)
continue
if data is not None:
sys.stdout.write(data)
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):
@@ -686,10 +921,17 @@ while inconsole or not doexit:
sys.stdout.write("\r\n[remote disconnected]\r\n")
break
else:
myinput = fh.read()
myinput = check_escape_seq(myinput, fh)
if myinput:
tlvdata.send(session.connection, myinput)
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()
else:
currcommand = prompt()
try:
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# This will take a given directory and make a 'big floppy image'
# out of it, suitable for nodemedia upload.
if [ -z "$1" -o -z "$2" ]; then
echo "Usage: $0 <directory> <imagefile>"
exit 1
fi
# Get the needed payload side
SIZE=$(du -sB 512 $1|awk '{print $1}')
ENTRIES=$(find $1 |wc -l)
# Also, each file and directory has overhead, pad size by 32kb per file,
# which should be overkill but other values proved to be inadequate
# A deeper understanding of VFAT would probably allow for more precise value
SIZE=$((SIZE + ENTRIES * 64))
dd if=/dev/zero of=$2 bs=512 count=$SIZE
# Make a big single sided floppy with many many tracks, saves on complex math
mformat -i $2 -d 1 -t $SIZE -s 1 -h 1 ::
mcopy -i $2 -s $1/* ::
+116
View File
@@ -0,0 +1,116 @@
#!/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.
__author__ = 'alin37'
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 [-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')
(options, args) = argparser.parse_args()
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
#Sets attributes
nodetype="noderange"
if len(args) > 1:
if "=" in args[1] or options.clear or options.environment:
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
exitcode=client.updateattrib(session,args,nodetype, noderange, options)
try:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
requestargs=args[2:]
elif args[1] == 'current':
showtype = 'current'
requestargs=args[2:]
else:
showtype = 'all'
requestargs=args[1:]
except:
pass
if exitcode != 0:
sys.exit(exitcode)
# Lists all attributes
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = client.printattributes(session, requestargs, showtype,nodetype, noderange, options)
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)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
errorNodes = set([])
success = session.simple_noderange_command(noderange, 'configuration/management_controller/reset', 'reset', key='state', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": BMC Reset Successful"
sys.exit(success)
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import 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()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
sys.stderr.write(
'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]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
exitcode = 0
if options.biosmode:
bootmode = 'bios'
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)
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import signal
import optparse
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
class NullOpt(object):
blame = None
clear = None
def bailout(msg, code=1):
sys.stderr.write(msg + '\n')
sys.exit(code)
argparser = optparse.OptionParser()
argparser.add_option('-c', '--comparedefault', dest='comparedefault',
action='store_true', default=False,
help='Compare given settings to default or list settings '
'that are non default')
argparser.add_option('-d', '--detail', dest='detail',
action='store_true', default=False,
help='Provide verbose information as available, such as '
'help text and possible valid values')
argparser.add_option('-x', '--exclude', dest='exclude',
action='store_true', default=False,
help='Treat positional arguments as items to not '
'examine, compare, or restore default')
(options, args) = argparser.parse_args()
cfgpaths = {
'bmc.ipv4_address': (
'configuration/management_controller/net_interfaces/management',
'ipv4_address'),
'bmc.ipv4_method': (
'configuration/management_controller/net_interfaces/management',
'ipv4_configuration'),
'bmc.ipv4_gateway': (
'configuration/management_controller/net_interfaces/management',
'ipv4_gateway'),
}
autodeps = {
'bmc.ipv4_address': (('bmc.ipv4_method', 'static'),)
}
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
setmode = None
assignment = {}
queryparms = {}
printsys = []
setsys = {}
forceset = False
needval = None
if len(args) == 1 or options.exclude:
if not options.exclude:
printsys = 'all'
for candidate in cfgpaths:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
def _assign_value():
if key not in cfgpaths:
setsys[key] = value
for depkey, depval in autodeps.get(key, []):
assignment[depkey] = depval
assignment[key] = value
for param in args[1:]:
if param == 'show':
continue # forgive muscle memory of pasu users
if param == 'set':
setmode = True
forceset = True
continue
if needval:
key = needval
value = param
_assign_value()
continue
if '=' in param or param[-1] == ':' or forceset:
if setmode is None:
setmode = True
if setmode != True:
bailout('Cannot do set and query in same command')
if '=' in param:
key, _, value = param.partition('=')
_assign_value()
elif param[-1] == ':':
needval = param[:-1]
else:
needval = param
else:
if setmode is None:
setmode = False
if setmode != False:
bailout('Cannot do set and query in same command')
if '.' not in param:
matchedparms = False
for candidate in cfgpaths:
if candidate.startswith('{0}.'.format(param)):
matchedparms = True
if not options.exclude:
path, attrib = cfgpaths[candidate]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = candidate
else:
try:
del queryparms[path]
except KeyError:
pass
if not matchedparms:
printsys.append(param)
elif param not in cfgpaths:
printsys.append(param)
else:
path, attrib = cfgpaths[param]
path = '/noderange/{0}/{1}'.format(noderange, path)
if path not in queryparms:
queryparms[path] = {}
queryparms[path][attrib] = param
session = client.Command()
rcode = 0
if setmode:
if options.exclude:
sys.stderr.write('Cannot use exclude and assign at the same time\n')
sys.exit(1)
updatebypath = {}
attrnamebypath = {}
for key in assignment:
if key not in cfgpaths:
path = 'configuration/system/all'
attrib = key
else:
path, attrib = cfgpaths[key]
if path not in updatebypath:
updatebypath[path] = {}
attrnamebypath[path] = {}
updatebypath[path][attrib] = assignment[key]
attrnamebypath[path][attrib] = key
# well, we want to expand things..
# check ipv4, if requested change method to static
for path in updatebypath:
for fr in session.update('/noderange/{0}/{1}'.format(noderange, path),
updatebypath[path]):
for node in fr['databynode']:
r = fr['databynode'][node]
if 'error' in r:
sys.stderr.write(node + ': ' + r['error'] + '\n')
if 'errorcode' in r:
rcode |= r['errorcode']
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
client.print_attrib_path(path, session, list(queryparms[path]),
NullOpt(), queryparms[path])
if printsys or options.exclude:
if printsys == 'all':
printsys = []
path = '/noderange/{0}/configuration/system/all'.format(noderange)
client.print_attrib_path(path, session, printsys,
options)
sys.exit(rcode)
Regular → Executable
View File
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
attribs[key] = val
for r in session.create('/noderange/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import csv
import optparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
tabformat = '{0:>15}|{1:>15}|{2:>15}|{3:>36}|{4:>17}|{5:>12}|{6:>48}'
columns = ['Node', 'Model', 'Serial', 'UUID', 'Mac Address', 'Type',
'Current IP Addresses']
delimit = ['-' * 15, '-' * 15, '-' * 15, '-' * 36, '-' * 17, '-' * 12,
'-' * 48]
def dumpmacs(procinfo):
return ','.join(procinfo['macs']) # + procinfo.get('relatedmacs', []))
def print_disco(options, session, currmac):
procinfo = {}
for tmpinfo in session.read('/discovery/by-mac/{0}'.format(currmac)):
procinfo.update(tmpinfo)
record = [procinfo['nodename'], procinfo['modelnumber'],
procinfo['serialnumber'], procinfo['uuid'], dumpmacs(procinfo),
','.join(procinfo['types']),
','.join(sorted(procinfo['ipaddrs']))]
if options.csv:
csv.writer(sys.stdout).writerow(record)
else:
print(tabformat.format(*record))
def process_header(header):
# normalize likely header titles
fields = []
broken = False
for datum in header:
datum = datum.lower()
if datum.startswith('node') or datum.startswith('name'):
fields.append('node')
elif datum in ('nodegroup', 'nodegroups', 'group', 'groups'):
fields.append('groups')
elif datum.startswith('mac') or datum.startswith('ether'):
fields.append('mac')
elif datum.startswith('serial') or datum in ('sn', 's/n'):
fields.append('serial')
elif datum == 'uuid':
fields.append('uuid')
elif datum in ('bmc', 'imm', 'xcc'):
fields.append('hardwaremanagement.manager')
elif datum in ('bmc gateway', 'xcc gateway', 'imm gateway'):
fields.append('net.bmc.gateway')
elif datum in ('bmcuser', 'username', 'user'):
fields.append('secret.hardwaremanagementuser')
elif datum in ('bmcpass', 'password', 'pass'):
fields.append('secret.hardwaremanagementpassword')
else:
print("Unrecognized column name {0}".format(datum))
broken = True
if broken:
sys.exit(1)
return tuple(fields)
def datum_complete(datum):
if 'node' not in datum or not datum['node']:
sys.stderr.write('Nodename is a required field')
return False
provided = set(datum)
required = set(['serial', 'uuid', 'mac'])
for field in provided & required:
if datum[field]:
break
else:
sys.stderr.write('One of the fields "Serial Number", "UUID", or '
'"MAC Address" must be provided')
return False
return True
searchkeys = set(['mac', 'serial', 'uuid'])
def search_record(datum, options, session):
for searchkey in searchkeys:
options.__dict__[searchkey] = None
for searchkey in searchkeys & set(datum):
options.__dict__[searchkey] = datum[searchkey]
return list(list_matching_macs(options, session))
def datum_to_attrib(datum):
for key in ('serial', 'uuid', 'mac'):
try:
del datum[key]
except KeyError:
pass
datum['name'] = datum['node']
del datum['node']
return datum
def import_csv(options, session):
nodedata = []
with open(options.importfile, 'r') as datasrc:
records = csv.reader(datasrc)
fields = process_header(next(records))
for record in records:
currfields = list(fields)
nodedatum = {}
for datum in record:
nodedatum[currfields.pop(0)] = datum
if not datum_complete(nodedatum):
sys.exit(1)
if not search_record(nodedatum, options, session):
sys.stderr.write(
"Could not match the following data: " +
repr(nodedatum) + '\n')
sys.exit(1)
nodedata.append(nodedatum)
for datum in nodedata:
maclist = search_record(datum, options, session)
datum = datum_to_attrib(datum)
nodename = datum['name']
for res in session.create('/nodes/', datum):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
continue
elif 'created' in res:
print('Defined ' + res['created'])
else:
print(repr(res))
for mac in maclist:
for res in session.update('/discovery/by-mac/{0}'.format(mac),
{'node': nodename}):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
continue
elif 'assigned' in res:
print('Discovered ' + res['assigned'])
else:
print(repr(res))
def list_discovery(options, session):
if options.csv:
csv.writer(sys.stdout).writerow(columns)
else:
print(tabformat.format(*columns))
print(tabformat.format(*delimit))
for mac in list_matching_macs(options, session):
print_disco(options, session, mac)
def clear_discovery(options, session):
for mac in list_matching_macs(options, session):
for res in session.delete('/discovery/by-mac/{0}'.format(mac)):
if 'deleted' in res:
print('Cleared info for {0}'.format(res['deleted']))
else:
print(repr(res))
def list_matching_macs(options, session):
path = '/discovery/'
if options.model:
path += 'by-model/{0}/'.format(options.model)
if options.serial:
path += 'by-serial/{0}/'.format(options.serial)
if options.uuid:
path += 'by-uuid/{0}/'.format(options.uuid)
if options.type:
path += 'by-type/{0}/'.format(options.type)
if options.state:
if options.state == 'unknown':
options.state = 'unidentified'
path += 'by-state/{0}/'.format(options.state).lower()
if options.mac:
path += 'by-mac/{0}'.format(options.mac)
result = list(session.read(path))[0]
if 'error' in result:
return []
return [options.mac.replace(':', '-')]
else:
path += 'by-mac/'
return [x['item']['href'] for x in session.read(path)]
def assign_discovery(options, session):
abort = False
if options.importfile:
return import_csv(options, session)
if not (options.serial or options.uuid or options.mac):
sys.stderr.write(
"UUID (-u), serial (-s), or ether address (-e) required for "
"assignment\n")
abort = True
if not options.node:
sys.stderr.write("Node (-n) must be specified for assignment\n")
abort = True
if abort:
sys.exit(1)
matches = list_matching_macs(options, session)
if not matches:
sys.stderr.write("No matching discovery candidates found\n")
sys.exit(1)
for res in session.update('/discovery/by-mac/{0}'.format(matches[0]),
{'node': options.node}):
if 'assigned' in res:
print('Assigned: {0}'.format(res['assigned']))
else:
print(repr(res))
def main():
parser = optparse.OptionParser(
usage='Usage: %prog [list|assign|rescan|clear] [options]')
# -a for 'address' maybe?
# order by
# show state (discovered or..
# nodediscover approve?
# flush to clear old data out? (e.g. no good way to age pxe data)
# also delete discovery datum... more targeted
# defect: -t lenovo-imm returns all
parser.add_option('-m', '--model', dest='model',
help='Operate with nodes matching the specified model '
'number', metavar='MODEL')
parser.add_option('-d', '--discoverystate', dest='state',
help='The discovery state of the entries (discovered, '
'identified, and unidentified)')
parser.add_option('-s', '--serial', dest='serial',
help='Operate against the system matching the specified '
'serial number', metavar='SERIAL')
parser.add_option('-u', '--uuid', dest='uuid',
help='Operate against the system matching the specified '
'UUID', metavar='UUID')
parser.add_option('-n', '--node', help='Operate with the given nodename')
parser.add_option('-e', '--ethaddr', dest='mac',
help='Operate against the system with the specified MAC '
'address', metavar='MAC')
parser.add_option('-t', '--type', dest='type',
help='Operate against the system of the specified type',
metavar='TYPE')
parser.add_option('-c', '--csv', dest='csv',
help='Use CSV formatted output', action='store_true')
parser.add_option('-i', '--import', dest='importfile',
help='Import bulk assignment data from given CSV file',
metavar='IMPORT.CSV')
(options, args) = parser.parse_args()
if len(args) == 0 or args[0] not in ('list', 'assign', 'rescan', 'clear'):
parser.print_help()
sys.exit(1)
session = client.Command()
if args[0] == 'list':
list_discovery(options, session)
if args[0] == 'clear':
clear_discovery(options, session)
if args[0] == 'assign':
assign_discovery(options, session)
if args[0] == 'rescan':
list(session.update('/discovery/rescan', {'rescan': 'start'}))
print("Rescan initiated")
if __name__ == '__main__':
main()
+25 -10
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,10 +15,17 @@
# 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
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'):
@@ -26,34 +33,42 @@ 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()
try:
noderange = sys.argv[1]
noderange = args[0]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> [clear]\n'.format(
sys.argv[0]))
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
def format_event(evt):
retparts = []
if 'timestamp' in evt:
if 'timestamp' in evt and evt['timestamp'] is not None:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
dscparts = []
if 'component_type' in evt:
if 'component_type' in evt and evt['component_type'] is not None:
dscparts.append(evt['component_type'])
if 'component' in evt and evt['component'] is not None:
dscparts.append(evt['component'])
if 'event' in evt:
if 'event' in evt and evt['event'] and evt['event'] is not None:
evttext = evt['event']
try:
if evttext.startswith(evt['component'] + ' - '):
@@ -83,4 +98,4 @@ for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
if 'events' in thisdata:
evtdata = thisdata['events']
for evt in evtdata:
print '{0}: {1}'.format(node, format_event(evt))
print '{0}: {1}'.format(node, format_event(evt))
+170
View File
@@ -0,0 +1,170 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import 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 printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
def printfirm(node, prefix, data):
if 'model' in data:
prefix += ' ' + data['model']
builddesc = []
if 'build' in data:
builddesc.append(data['build'])
if 'date' in data:
builddesc.append(data['date'])
if 'version' in data:
version = data['version']
if builddesc:
version += ' ({0})'.format(' '.join(builddesc))
else:
version = ' '.join(builddesc)
print('{0}: {1}: {2}'.format(node, prefix, version))
components = ['all']
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [update [--backup <file>]]|[<components>]")
argparser.add_option('-b', '--backup', action='store_true',
help='Target a backup bank rather than primary')
(options, args) = argparser.parse_args()
upfile = None
try:
noderange = args[0]
if len(args) > 1:
if args[1] == 'update':
upfile = args[2]
else:
components = []
for arg in args[1:]:
components += arg.split(',')
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def get_update_progress(session, url):
for res in session.read(url):
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
text = '{0}: {1:3.0f}%'.format(status, percent)
return text, status, detail
def update_firmware(session, filename):
global exitcode
output = sq.ScreenPrinter(noderange, session)
nodeurls = {}
filename = os.path.abspath(filename)
resource = '/noderange/{0}/inventory/firmware/updates/active'.format(
noderange)
upargs = {'filename': filename}
if options.backup:
upargs['bank'] = 'backup'
noderrs = {}
for res in session.create(resource, upargs):
if 'created' not in res:
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
'error', 'Unknown Error')
continue
watchurl = res['created']
currnode = watchurl.split('/')[1]
nodeurls[currnode] = '/' + watchurl
while nodeurls:
for node in list(nodeurls):
progress, status, err = get_update_progress(
session, nodeurls[node])
if status == 'error':
exitcode = 1
noderrs[node] = err
if status in ('error', 'complete', 'pending'):
list(session.delete(nodeurls[node]))
del nodeurls[node]
output.set_output(node, progress)
time.sleep(2)
allerrnodes = ','.join(noderrs)
if noderrs:
sys.stderr.write(
'Nodes had errors updating ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
def show_firmware(session):
firmware_shown = False
for component in components:
for res in session.read(
'/noderange/{0}/inventory/firmware/all/{1}'.format(
noderange, component)):
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:
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)
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
+113
View File
@@ -0,0 +1,113 @@
#!/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.
__author__ = 'alin37'
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 [options] \
\n %prog [options] nodegroup [list of attributes] \
\n %prog [options] nodegroup nodes=value1,value2 \
\n %prog -e nodegroup <attribute names to set> \
\n %prog [options] nodegroup nodes=value1,value2
\n ''')
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
argparser.add_option('-e', '--environment', action='store_true',
help='Set attributes, but from environment variable of '
'same name')
argparser.add_option('-c', '--clear', action='store_true',
help='Clear variables')
(options, args) = argparser.parse_args()
#setting minimal output to only output current information
showtype = 'current'
requestargs=None
nodetype="nodegroups"
try:
nodegroups = args[0]
nodelist = '/{0}/{1}/'.format(nodetype,nodegroups)
except IndexError:
nodelist = '/nodegroups/'
client.check_globbing(nodegroups)
session = client.Command()
exitcode = 0
#Sets attributes
if len(args) > 1:
showtype = 'all'
if "=" in args[1] and options.clear:
print("Can not clear and set at the same time!")
argparser.print_help()
sys.exit(1)
exitcode=client.updateattrib(session,args,nodetype, nodegroups, options)
try:
# setting user output to what the user inputs
if args[1] == 'all':
showtype = 'all'
elif args[1] == 'current':
showtype = 'current'
requestargs=args[1:]
except Exception as e:
print str(e)
if exitcode != 0:
sys.exit(exitcode)
# Lists all attributes
if len(args) > 0:
# setting output to all so it can search since if we do have something to search, we want to show all outputs even if it is blank.
if requestargs is None:
showtype = 'current'
elif requestargs == []:
#showtype already set
pass
else:
try:
requestargs.remove('all')
requestargs.remove('current')
except ValueError:
pass
exitcode = client.printgroupattributes(session, requestargs, showtype,nodetype, nodegroups, options)
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)
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange attribute1=value1 attribute2=value,...
\n ''')
(options, args) = argparser.parse_args()
requestargs=None
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
attribs = {'name': noderange}
for arg in args[1:]:
key, val = arg.split('=')
attribs[key] = val
for r in session.create('/nodegroups/', attribs):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'created' in r:
print('{0}: created'.format(r['created']))
sys.exit(exitcode)
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange
\n ''')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for r in session.delete('/nodegroups/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+12 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,9 +16,15 @@
# limitations under the License.
import codecs
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'):
@@ -28,11 +34,14 @@ import confluent.client as client
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
noderange = args[0]
except IndexError:
sys.stderr.write('Usage: {0} <noderange>\n'.format(sys.argv[0]))
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
def main():
+15 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,8 +15,14 @@
# 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'))
@@ -25,15 +31,20 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
noderange = args[0]
except IndexError:
sys.stderr.write('Usage: {0} <noderange> [on|off]\n'.format(sys.argv[0]))
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)
session = client.Command()
exitcode = 0
sys.exit(
+155
View File
@@ -0,0 +1,155 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import codecs
import optparse
import os
import re
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
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
filters = []
def pretty(text):
if text == 'pcislot':
return 'PCI slot'
if text == 'partnumber':
return 'part number'
return text
def print_mem_info(node, prefix, meminfo):
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))
print('{0}: {1} manufacturer: {2}'.format(
node, prefix, meminfo['manufacturer']))
print('{0}: {1} model: {2}'.format(node, prefix, meminfo['model']))
print('{0}: {1} serial number: {2}'.format(node, prefix,
meminfo['serial']))
print('{0}: {1} manufacture date: {2}'.format(node, prefix,
meminfo['manufacture_date']))
print('{0}: {1} manufacture location: {2}'.format(
node, prefix, meminfo['manufacture_location']))
exitcode = 0
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
url = '/noderange/{0}/inventory/hardware/all/all'
argparser = optparse.OptionParser(
usage="Usage: %prog <noderange> [serial|model|uuid|mac]")
(options, args) = argparser.parse_args()
try:
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'
try:
session = client.Command()
for res in session.read(url.format(noderange)):
printerror(res)
if 'databynode' not in res:
continue
for node in res['databynode']:
printerror(res['databynode'][node], node)
if 'inventory' not in res['databynode'][node]:
continue
for inv in res['databynode'][node]['inventory']:
prefix = inv['name']
if not inv['present']:
if not filters:
print '{0}: {1}: Not Present'.format(node, prefix)
continue
info = inv['information']
info.pop('board_extra', None)
info.pop('oem_parser', None)
info.pop('chassis_extra', None)
info.pop('product_extra', None)
if 'memory_type' in info:
if not filters:
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
print(u'{0}: {1} {2}: {3}'.format(node, prefix,
pretty(datum),
info[datum]))
except KeyboardInterrupt:
print('')
sys.exit(exitcode)
Regular → Executable
+40 -61
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,76 +15,55 @@
# See the License for the specific language governing permissions and
# limitations under the License.
__author__ = 'jjohnson2'
__author__ = 'jjohnson2,alin37'
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 [options] noderange [list of attributes")
argparser.add_option('-b', '--blame', action='store_true',
def main():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange\n"
" or: %prog [options] noderange <nodeattribute>...")
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
nodelist = '/nodes/'
session = client.Command()
exitcode = 0
if len(args) > 1:
seenattributes = set([])
for res in session.read('/noderange/{0}/attributes/all'.format(noderange)):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
continue
for node in res['databynode']:
for attr in res['databynode'][node]:
seenattributes.add(attr)
currattr = res['databynode'][node][attr]
if attr in args[1:]:
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
node, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(node, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node, attr)
else:
attrout = '{0}: {1}:'.format(node, attr)
if options.blame:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
currattr['expression']))
if blamedata:
attrout += ' (' + ', '.join(blamedata) + ')'
print attrout
if not exitcode:
for attr in args[1:]:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
(options, args) = argparser.parse_args()
noderange=""
nodelist=""
try:
noderange = args[0]
nodelist = '/noderange/{0}/nodes/'.format(noderange)
except IndexError:
nodelist = '/nodes/'
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
showtype='all'
requestargs=args[1:]
nodetype='noderange'
if len(args) > 1:
exitcode=client.printattributes(session,requestargs,showtype,nodetype,noderange,options)
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
for res in session.read(nodelist):
if 'error' in res:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
else:
print res['item']['href'].replace('/', '')
sys.exit(exitcode)
if __name__ == '__main__':
main()
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
import time
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
import confluent.screensqueeze as sq
exitcode = 0
def get_update_progress(session, url):
for res in session.read(url):
status = res['phase']
percent = res['progress']
detail = res['detail']
if status == 'error':
text = 'error!'
else:
text = '{0}: {1:3.0f}%'.format(status, percent)
return text, status, detail
def printerror(res, node=None):
global exitcode
if 'errorcode' in res:
exitcode = res['errorcode']
if 'error' in res:
if node:
sys.stderr.write('{0}: {1}\n'.format(node, res['error']))
else:
sys.stderr.write('{0}\n'.format(res['error']))
if 'errorcode' not in res:
exitcode = 1
for node in res.get('databynode', ()):
printerror(res['databynode'][node], node)
def attach_media(noderange, media):
global exitcode
session = client.Command()
if ':' not in media:
sys.stderr.write('Full URL required for attach\n')
sys.exit(1)
resource = '/noderange/{0}/media/attach'.format(noderange)
for res in session.update(resource, {'url': media}):
printerror(res)
list_media(noderange, media)
def list_media(noderange, media):
session = client.Command()
resource = '/noderange/{0}/media/current'.format(noderange)
for res in session.read(resource):
printerror(res)
for node in res.get('databynode', []):
url = res['databynode'][node].get('url', None)
name = res['databynode'][node].get('name', None)
if url and not res['databynode'][node].get('secure', False):
name += ' (insecure)'
if not name:
continue
print('{0}: {1}'.format(node, url + '/' + name if url else name))
def detach_media(noderange, media):
global exitcode
session = client.Command()
resource = '/noderange/{0}/media/detach'.format(noderange)
for res in session.update(resource, {'detachall': 1}):
printerror(res)
def upload_media(noderange, media):
global exitcode
if not os.path.exists(media):
sys.stderr.write('Unable to locate requested file {0}\n'.format(
media))
sys.exit(404)
session = client.Command()
output = sq.ScreenPrinter(noderange, session)
filename = os.path.abspath(media)
resource = '/noderange/{0}/media/uploads/'.format(noderange)
upargs = {'filename': filename}
noderrs = {}
nodeurls = {}
for res in session.create(resource, upargs):
if 'created' not in res:
for nodename in res.get('databynode', ()):
output.set_output(nodename, 'error!')
noderrs[nodename] = res['databynode'][nodename].get(
'error', 'Unknown Error')
continue
watchurl = res['created']
currnode = watchurl.split('/')[1]
nodeurls[currnode] = '/' + watchurl
while nodeurls:
for node in list(nodeurls):
progress, status, err = get_update_progress(
session, nodeurls[node])
if status == 'error':
exitcode = 1
noderrs[node] = err
if status in ('error', 'complete', 'pending'):
list(session.delete(nodeurls[node]))
del nodeurls[node]
output.set_output(node, progress)
time.sleep(2)
allerrnodes = ','.join(noderrs)
if noderrs:
sys.stderr.write(
'Nodes had errors receiving media ({0})!\n'.format(allerrnodes))
for node in noderrs:
sys.stderr.write('{0}: {1}\n'.format(node, noderrs[node]))
list_media(noderange, media)
funmap = {
'upload': upload_media,
'attach': attach_media,
'detachall': detach_media,
'list': list_media,
}
class OptParser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.expand_prog_name(self.epilog)
def main():
argparser = OptParser(
usage="Usage: %prog <noderange> [list|upload|attach|detachall] "
"<filename>|all|<url>",
epilog='\nupload will take the specified file and upload it to the '
'BMC.\n\n'
'attach will instruct the BMC to connect a remote media to the '
'specified url.\n\ndetachall will remove *ALL* uploaded and '
'attached urls from the BMC\n\nlist shows currently mounted '
'media.\n\nSee `man %prog` for more info.\n')
(options, args) = argparser.parse_args()
media = None
try:
noderange = args[0]
operation = args[1]
arglength = 2
if operation in ('attach', 'upload'):
media = args[2]
arglength = 3
if len(args) > arglength:
argparser.print_help()
sys.exit(1)
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
try:
handler = funmap[operation]
except KeyError:
argparser.print_help()
sys.exit(1)
handler(noderange, media)
if __name__ == '__main__':
main()
+37 -9
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,9 +15,15 @@
# 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'):
@@ -25,15 +31,19 @@ if path.startswith('/opt'):
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 = sys.argv[1]
noderange = args[0]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> ([status|on|off|shutdown|boot|reset]\n'.format(
sys.argv[0]))
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
setstate = None
if len(sys.argv) > 2:
if setstate == 'softoff':
@@ -41,7 +51,25 @@ if len(sys.argv) > 2:
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()
sys.exit(1)
session = client.Command()
exitcode = 0
sys.exit(
session.simple_noderange_command(noderange, '/power/state', setstate))
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))
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage='''\n %prog noderange
\n ''')
(options, args) = argparser.parse_args()
if len(args) != 1:
argparser.print_help()
sys.exit(1)
noderange = args[0]
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
for r in session.delete('/noderange/{0}'.format(noderange)):
if 'error' in r:
sys.stderr.write(r['error'] + '\n')
exitcode |= 1
if 'deleted' in r:
print('{0}: deleted'.format(r['deleted']))
sys.exit(exitcode)
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import signal
import sys
try:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
except AttributeError:
pass
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.exit(1)
client.check_globbing(noderange)
session = client.Command()
exitcode = 0
errorNodes = set([])
success = session.simple_noderange_command(noderange, 'power/reseat', 'reseat', key='reseat', errnodes=errorNodes) # = 0 if successful
# Determine which nodes were successful and print them
allNodes = set([])
for node in session.read('/noderange/{0}/nodes/'.format(noderange)):
allNodes.add(node['item']['href'].replace("/", ""))
goodNodes = allNodes - errorNodes
for node in goodNodes:
print node + ": Reseat successful"
sys.exit(success)
+129
View File
@@ -0,0 +1,129 @@
#!/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.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
concurrentprocs = options.count
client.check_globbing(args[0])
c = client.Command()
cmdstr = " ".join(args[1:])
currprocs = 0
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', ())
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)
while all:
pernodeout = {}
for r in rdy:
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
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()
+27 -7
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,11 +16,17 @@
# limitations under the License.
import csv
import datetime
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'):
@@ -30,6 +36,7 @@ import confluent.client as client
sensorcollections = {
'all': 'sensors/hardware/all/all',
'energy': 'sensors/hardware/energy/all',
'temperature': 'sensors/hardware/temperature/all',
'temp': 'sensors/hardware/temperature/all',
'power': 'sensors/hardware/power/all',
@@ -39,13 +46,15 @@ sensorcollections = {
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [sensor(s)")
argparser.add_option('-i', '--interval', type='int',
usage="Usage: %prog [options] noderange ([sensor(s)])")
argparser.add_option('-i', '--interval', type='float',
help='Interval to do repeated samples over')
argparser.add_option('-n', '--numreadings', type='int',
help='Number of readings to gather')
argparser.add_option('-c', '--csv', action='store_true',
help='Output in CSV format')
argparser.add_option('-s', '--skipnumberless', action='store_true',
help='Output in CSV format')
(options, args) = argparser.parse_args()
repeatmode = False
if options.interval:
@@ -56,8 +65,9 @@ if options.numreadings:
options.interval = 1
try:
noderange = args[0]
client.check_globbing(noderange)
except IndexError:
argparser.print_usage()
argparser.print_help()
sys.exit(1)
sensors = []
for sensorgroup in args[1:]:
@@ -70,7 +80,6 @@ for sensorgroup in args[1:]:
sensors.append('sensors/hardware/all/' + sensor)
if not sensors:
sensors = ['sensors/hardware/all/all']
session = client.Command()
exitcode = 0
sensorheaders = {}
@@ -101,6 +110,8 @@ def sensorpass(showout=True, appendtime=False):
if 'sensors' not in reading[node]:
continue
for sensedata in reading[node]['sensors']:
if sensedata['value'] is None and options.skipnumberless:
continue
for redundant_state in ('Non-Critical', 'Critical'):
try:
sensedata['states'].remove(redundant_state)
@@ -115,6 +126,8 @@ def sensorpass(showout=True, appendtime=False):
if showout:
if sensedata['value'] is None:
showval = ''
elif isinstance(sensedata['value'], float):
showval = u' {0:.5f} '.format(sensedata['value'])
else:
showval = u' {0} '.format(sensedata['value'])
if sensedata['units'] not in (None, u''):
@@ -136,13 +149,19 @@ 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
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
for nodekey in resdata:
if showtime:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
if showtime.is_integer():
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
else:
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S.') +
str(datetime.datetime.now().microsecond//1000),
nodekey]
else:
rowdata = [nodekey]
for sensorkey in orderedsensors:
@@ -193,7 +212,8 @@ def main():
nextstart = os.times()[4] + options.interval
resdata = sensorpass(linebyline, True)
if options.csv:
format_csv(csvwriter, orderedsensors, resdata)
format_csv(csvwriter, orderedsensors, resdata,
showtime=options.interval)
if options.numreadings:
options.numreadings -= 1
if options.numreadings <= 0:
+30 -6
View File
@@ -15,9 +15,15 @@
# 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'):
@@ -25,13 +31,26 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='Usage: %prog [options] noderange [default|cd|network|setup|hd]')
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
argparser.add_option('-u', '--uefi', dest='uefi', action='store_true',
default=True,
help='Request UEFI style boot (rather than BIOS)')
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
noderange = args[0]
client.check_globbing(noderange)
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
sys.argv[0]))
argparser.print_help()
sys.exit(1)
bootdev = None
if len(sys.argv) > 2:
@@ -40,5 +59,10 @@ if len(sys.argv) > 2:
bootdev = 'network'
session = client.Command()
exitcode = 0
sys.exit(
session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev))
if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
+130
View File
@@ -0,0 +1,130 @@
#!/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 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.sortutil as sortutil
def run():
argparser = optparse.OptionParser(
usage="Usage: %prog noderange commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.add_option('-f', '-c', '--count', type='int', default=168,
help='Number of commands to run at a time')
# among other things, FD_SETSIZE limits. Besides, spawning too many
# processes can be unkind for the unaware on memory pressure and such...
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
client.check_globbing(args[0])
concurrentprocs = options.count
c = client.Command()
cmdstr = " ".join(args[1:])
currprocs = 0
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', ())
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = ['ssh', node, 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)
while all:
pernodeout = {}
for r in rdy:
desc = pipedesc[r]
node = desc['node']
data = True
while data and select.select([r], [], [], 0):
data = r.readline()
if data:
if desc['type'] == 'stdout':
if node not in pernodeout:
pernodeout[node] = []
pernodeout[node].append(data)
else:
sys.stderr.write('{0}: {1}'.format(node, data))
sys.stderr.flush()
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
r.close()
if desc['type'] == 'stdout' and pendingexecs:
node, cmdv = pendingexecs.popleft()
run_cmdv(node, cmdv, all, pipedesc)
for node in sortutil.natural_sort(pernodeout):
for line in pernodeout[node]:
sys.stdout.write('{0}: {1}'.format(node, line))
sys.stdout.flush()
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
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()
+360 -14
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ import anydbm as dbm
import errno
import hashlib
import os
import shlex
import socket
import ssl
import sys
@@ -26,6 +27,15 @@ import confluent.tlvdata as tlvdata
SO_PASSCRED = 16
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
}
def cprint(txt):
print(txt)
sys.stdout.flush()
def _parseserver(string):
if ']:' in string:
@@ -42,9 +52,11 @@ 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']
@@ -74,8 +86,16 @@ class Command(object):
if authdata['authpassed'] == 1:
self.authenticated = True
def handle_results(self, ikey, rc, res):
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):
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']
@@ -86,6 +106,8 @@ class Command(object):
res = res['databynode']
for node in res:
if 'error' in res[node]:
if errnodes is not None:
errnodes.add(node)
sys.stderr.write('{0}: Error: {1}\n'.format(
node, res[node]['error']))
if 'errorcode' in res[node]:
@@ -93,12 +115,26 @@ class Command(object):
else:
rc |= 1
elif ikey in res[node]:
print('{0}: {1}'.format(node, res[node][ikey]['value']))
if 'value' in res[node][ikey]:
val = res[node][ikey]['value']
elif 'isset' in res[node][ikey]:
val = '********' if res[node][ikey] else ''
else:
val = repr(res[node][ikey])
if self._prevkeyname and self._prevkeyname in res[node]:
cprint('{0}: {2}->{1}'.format(
node, val, res[node][self._prevkeyname]['value']))
elif self._prevdict and node in self._prevdict:
cprint('{0}: {2}->{1}'.format(
node, val, self._prevdict[node]))
else:
cprint('{0}: {1}'.format(node, val))
return rc
def simple_noderange_command(self, noderange, resource, input=None,
key=None):
key=None, errnodes=None, **kwargs):
try:
self._currnoderange = noderange
rc = 0
if resource[0] == '/':
resource = resource[1:]
@@ -110,28 +146,60 @@ 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)
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
return rc
except KeyboardInterrupt:
cprint('')
return 0
def simple_nodegroups_command(self, noderange, resource, input=None, key=None, **kwargs):
try:
rc = 0
if resource[0] == '/':
resource = resource[1:]
# The implicit key is the resource basename
if key is None:
ikey = resource.rpartition('/')[-1]
else:
ikey = key
if input is None:
for res in self.read('/nodegroups/{0}/{1}'.format(
noderange, resource)):
rc = self.handle_results(ikey, rc, res)
else:
for res in self.update('/noderange/{0}/{1}'.format(
noderange, resource), {ikey: input}):
kwargs[ikey] = input
for res in self.update('/nodegroups/{0}/{1}'.format(
noderange, resource), kwargs):
rc = self.handle_results(ikey, rc, res)
return rc
except KeyboardInterrupt:
print('')
cprint('')
return 0
def read(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('retrieve', path, self.connection, parameters)
def update(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('update', path, self.connection, parameters)
def create(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('create', path, self.connection, parameters)
def delete(self, path, parameters=None):
if not self.authenticated:
raise Exception('Unauthenticated')
return send_request('delete', path, self.connection, parameters)
def _connect_unix(self):
@@ -183,7 +251,7 @@ class Command(object):
if knownhosts:
certdata = self.connection.getpeercert(binary_form=True)
fingerprint = 'sha512$' + hashlib.sha512(certdata).hexdigest()
hostid = '@'.join((port,server))
hostid = '@'.join((port, server))
khf = dbm.open(os.path.join(clientcfgdir, "knownhosts"), 'c', 384)
if hostid in khf:
if fingerprint == khf[hostid]:
@@ -193,11 +261,10 @@ class Command(object):
"MISMATCHED CERTIFICATE DATA, ACCEPT NEW? (y/n):")
if replace not in ('y', 'Y'):
raise Exception("BAD CERTIFICATE")
print 'Adding new key for %s:%s' % (server, port)
cprint('Adding new key for %s:%s' % (server, port))
khf[hostid] = fingerprint
def send_request(operation, path, server, parameters=None):
"""This function iterates over all the responses
received from the server.
@@ -214,8 +281,287 @@ def send_request(operation, path, server, parameters=None):
tlvdata.send(server, payload)
result = tlvdata.recv(server)
while '_requestdone' not in result:
yield result
try:
yield result
except GeneratorExit:
while '_requestdone' not in result:
result = tlvdata.recv(server)
raise
result = tlvdata.recv(server)
def attrrequested(attr, attrlist, seenattributes):
for candidate in attrlist:
truename = candidate
if candidate.startswith('hm'):
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
if candidate in _attraliases:
candidate = _attraliases[candidate]
if candidate.lower() == attr.lower():
seenattributes.add(truename)
return True
elif attr.lower().startswith(candidate.lower() + '.'):
seenattributes.add(truename)
return True
return False
def printattributes(session, requestargs, showtype, nodetype, noderange, options):
path = '/{0}/{1}/attributes/{2}'.format(nodetype, noderange, showtype)
return print_attrib_path(path, session, requestargs, options)
def print_attrib_path(path, session, requestargs, options, rename=None):
exitcode = 0
seenattributes = set([])
for res in session.read(path):
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):
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 '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)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(node,
printattr)
else:
attrout = '{0}: {1}:'.format(node, printattr)
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(node, printattr,
currattr['broken'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(node, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
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)))
else:
cprint("CODE ERROR" + repr(attr))
try:
blame = options.blame
except AttributeError:
blame = False
if blame or 'broken' in currattr:
blamedata = []
if 'inheritedfrom' in currattr:
blamedata.append('inherited from group {0}'.format(
currattr['inheritedfrom']
))
if 'expression' in currattr:
blamedata.append(
'derived from expression "{0}"'.format(
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'])):
cprint('{0}: {1}: {2} (Default: {3})'.format(
node, printattr, currattr['value'],
currattr['default']))
else:
try:
details = options.detail
except AttributeError:
details = False
if details:
if currattr.get('help', None):
attrout += ' (Help: {0})'.format(
currattr['help'].encode('utf-8'))
if currattr.get('possible', None):
attrout += ' (Choices: {0})'.format(
','.join(currattr['possible']))
cprint(attrout)
if not exitcode:
if requestargs:
for attr in requestargs:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
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([])
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 attr in res:
seenattributes.add(attr)
currattr = res[attr]
if (requestargs is None or requestargs == [] or attrrequested(attr, requestargs, seenattributes)):
if 'value' in currattr:
if currattr['value'] is not None:
attrout = '{0}: {1}: {2}'.format(
noderange, attr, currattr['value'])
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif 'isset' in currattr:
if currattr['isset']:
attrout = '{0}: {1}: ********'.format(noderange, attr)
else:
attrout = '{0}: {1}:'.format(noderange, attr)
elif 'broken' in currattr:
attrout = '{0}: {1}: *ERROR* BROKEN EXPRESSION: ' \
'{2}'.format(noderange, attr,
currattr['broken'])
elif 'expression' in currattr:
attrout = '{0}: {1}: (will derive from expression {2})'.format(noderange, attr, currattr['expression'])
elif isinstance(currattr, list) or isinstance(currattr, tuple):
attrout = '{0}: {1}: {2}'.format(noderange, attr, ','.join(map(str, currattr)))
elif isinstance(currattr, dict):
dictout = []
for k, v in currattr.items:
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)
if not exitcode:
if requestargs:
for attr in requestargs:
if attr not in seenattributes:
sys.stderr.write('Error: {0} not a valid attribute\n'.format(attr))
exitcode = 1
return exitcode
def updateattrib(session, updateargs, nodetype, noderange, options):
# update attribute
exitcode = 0
if options.clear:
targpath = '/{0}/{1}/attributes/all'.format(nodetype, noderange)
keydata = {}
for attrib in updateargs[1:]:
keydata[attrib] = None
for res in session.update(targpath, keydata):
if 'error' in res:
if 'errorcode' in res:
exitcode = res['errorcode']
sys.stderr.write('Error: ' + res['error'] + '\n')
sys.exit(exitcode)
elif hasattr(options, 'environment') and options.environment:
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
# Let's do one pass to make sure that there's not a usage problem
for key in updateargs[1:]:
key = key.replace('.', '_')
value = os.environ.get(
key, os.environ[key.upper()])
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange,
'attributes/all',
value, key)
else:
exitcode = session.simple_noderange_command(noderange,
'attributes/all',
value, key)
sys.exit(exitcode)
else:
if "=" in updateargs[1]:
try:
for val in updateargs[1:]:
val = val.split('=')
if val[0][-1] in (',', '-', '^'):
key = val[0][:-1]
if val[0][-1] == ',':
value = {'prepend': val[1]}
elif val[0][-1] in ('-', '^'):
value = {'remove': val[1]}
else:
key = val[0]
value = val[1]
if (nodetype == "nodegroups"):
exitcode = session.simple_nodegroups_command(noderange, 'attributes/all',
value, key)
else:
exitcode = session.simple_noderange_command(noderange, 'attributes/all',
value, key)
except:
sys.stderr.write('Error: {0} not a valid expression\n'.format(str(updateargs[1:])))
exitcode = 1
sys.exit(exitcode)
return exitcode
# So we try to prevent bad things from happening when globbing
# We tried to head this off at the shell, but the various solutions would end
# up breaking the shell in various ways (breaking pipe capability if using
# DEBUG, breaking globbing if in pipe, etc)
# Then we tried to parse the original commandline instead, however shlex isn't
# going to parse full bourne language (e.g. knowing that '|' and '>' and
# a world of other things would not be in our command line
# so finally, just make sure the noderange appears verbatim in the command line
# if we glob to something, then bash will change noderange and this should
# detect it and save the user from tragedy
def check_globbing(noderange):
if not os.path.exists(noderange):
return True
rawargs = os.environ.get('CURRENT_CMDLINE', None)
if rawargs:
rawargs = shlex.split(rawargs)
for arg in rawargs:
if arg.startswith('$'):
arg = arg[1:]
if arg.endswith(';'):
arg = arg[:-1]
arg = os.environ.get(arg, '$' + arg)
if arg.startswith(noderange):
break
else:
sys.stderr.write(
'Shell glob conflict detected, specified target "{0}" '
'not in command line, but is a file. You can use "set -f" in '
'bash or change directories such that there is no filename '
'that would conflict.'
'\n'.format(noderange))
sys.exit(1)
@@ -0,0 +1,85 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import fcntl
import sys
import struct
import termios
def get_screenwidth():
return struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,
b'....'))[1]
class ScreenPrinter(object):
def __init__(self, noderange, client, textlen=4):
self.squeeze = sys.stdout.isatty()
self.textlen = textlen
self.noderange = noderange
self.client = client
self.nodeoutput = {}
self.nodelist = []
maxlen = 0
for ans in self.client.read('/noderange/{0}/nodes/'.format(noderange)):
if 'error' in ans:
sys.stderr.write(ans['error'])
continue
nodename = ans['item']['href'][:-1]
if len(nodename) > maxlen:
maxlen = len(nodename)
self.nodelist.append(nodename)
self.nodeoutput[nodename] = ''
self.nodenamelen = maxlen
self.textlen = textlen
self.fieldwidth = maxlen + textlen + 1 # 1 for column
def set_output(self, node, text):
self.nodeoutput[node] = text
if len(text) >= self.textlen:
self.textlen = len(text) + 1
self.fieldwidth = self.textlen + self.nodenamelen + 1
self.drawscreen()
def drawscreen(self):
if self.squeeze:
currwidth = get_screenwidth()
numfields = currwidth // self.fieldwidth
fieldformat = '{{0:>{0}}}:{{1:{1}}}'.format(self.nodenamelen,
self.textlen)
sys.stdout.write('\x1b[2J\x1b[;H') # clear screen
else:
numfields = 1
fieldformat = '{0}: {1}'
currfields = 0
for node in self.nodelist:
if currfields >= numfields:
sys.stdout.write('\n')
currfields = 1
else:
currfields += 1
sys.stdout.write(fieldformat.format(node, self.nodeoutput[node]))
sys.stdout.write('\n')
sys.stdout.flush()
if __name__ == '__main__':
import confluent.client as client
c = client.Command()
p = ScreenPrinter('n1-n13', c)
p.set_output('n3', 'Upload: 67%')
+43
View File
@@ -0,0 +1,43 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
numregex = re.compile('([0-9]+)')
def naturalize_string(key):
"""Analyzes string in a human way to enable natural sort
:param key: The node name to analyze
:returns: A structure that can be consumed by 'sorted'
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(numregex, key)]
def natural_sort(iterable):
"""Return a sort using natural sort if possible
:param iterable:
:return:
"""
try:
return sorted(iterable, key=naturalize_string)
except TypeError:
# The natural sort attempt failed, fallback to ascii sort
return sorted(iterable)
+217
View File
@@ -0,0 +1,217 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import difflib
import re
import sys
try:
range = xrange
except NameError:
pass
numregex = re.compile('([0-9]+)')
def humanify_nodename(nodename):
"""Analyzes nodename in a human way to enable natural sort
:param nodename: The node name to analyze
:returns: A structure that can be consumed by 'sorted'
"""
return [int(text) if text.isdigit() else text.lower()
for text in re.split(numregex, nodename)]
def _colorize_line(orig, mask):
highlighted = False
newline = orig[0]
for i in range(1, len(orig)):
if i > len(mask) - 1:
if highlighted:
newline += '\x1b[0m'
newline += orig[i:]
break
if highlighted and mask[i] == ' ':
highlighted = False
newline += '\x1b[0m'
elif not highlighted and mask[i] != ' ':
highlighted = True
newline += '\x1b[31m'
newline += orig[i]
newline += '\x1b[0m'
return newline
def near_diff(idx, diffdata):
maxidx = idx + 3
if maxidx > len(diffdata) -1:
maxidx = len(diffdata) - 1
idx = idx - 3
if idx < 0:
idx = 0
while idx <= maxidx:
if len(diffdata[idx]) > 0 and diffdata[idx][0] in ('+', '-'):
return True
idx += 1
return False
def colordiff(first, second):
diffdata = list(difflib.ndiff(first, second))
quiet = True
for i in range(len(diffdata)):
if i < len(diffdata) - 1 and diffdata[i + 1].startswith('?'):
if quiet:
quiet = False
yield '@@'
yield _colorize_line(diffdata[i], diffdata[i + 1])
elif not diffdata[i].startswith('?') and near_diff(i, diffdata):
if quiet:
quiet = False
yield '@@'
yield diffdata[i]
elif not diffdata[i].startswith('?'):
quiet = True
class GroupedData(object):
'''A post processor to sort and compare per-node data
:param confluentconnection: If given, will attempt to use the connection to abbreviate noderanges
'''
def __init__(self, confluentconnection=None):
self.bynode = {}
self.byoutput = {}
self.header = {}
self.client = confluentconnection
def generate_byoutput(self):
self.byoutput = {}
for n in self.bynode:
output = '\n'.join(self.bynode[n])
if output not in self.byoutput:
self.byoutput[output] = set([n])
else:
self.byoutput[output].add(n)
def add_line(self, node, line):
if node not in self.bynode:
self.bynode[node] = [line]
else:
self.bynode[node].append(line)
def get_group_text(self, nodes):
if self.client:
headerkey = ','.join(sorted(nodes))
if headerkey in self.header:
return self.header[headerkey]
noderange = ''
for reply in self.client.create('/noderange//abbreviate',
{'nodes': sorted(nodes)}):
noderange = reply['noderange']
self.header[headerkey] = noderange
return noderange
else:
return ','.join(sorted(nodes, key=humanify_nodename))
def print_all(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
self.generate_byoutput()
modaloutput = None
ismodal = True
if reverse:
outdatalist = sorted(
self.byoutput, key=lambda x: [len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))])
else:
outdatalist = sorted(
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))])
if reverse and skipmodal:
# if reversed, the last is biggest and should be skipped if modal
outdatalist = outdatalist[:-1]
for outdata in outdatalist:
if not reverse and skipmodal:
# If big first, this makes skipmodal skip first
skipmodal = False
continue
currout = '====================================\n'
currout += self.get_group_text(self.byoutput[outdata])
currout += '\n====================================\n'
if count:
currout += 'Count: {0}'.format(len(list(
self.byoutput[outdata])))
currout += '\n====================================\n'
currout += outdata
currout += '\n\n'
output.write(currout)
output.flush()
def print_deviants(self, output=sys.stdout, skipmodal=False, reverse=False,
count=False):
self.generate_byoutput()
modaloutput = None
ismodal = True
revoutput = []
for outdata in sorted(
self.byoutput, key=lambda x: [0 - len(self.byoutput[x]),
humanify_nodename(
self.get_group_text(
self.byoutput[x]
))]):
if modaloutput is None:
modaloutput = outdata
if skipmodal:
skipmodal = False
ismodal = False
continue
currout = '====================================\n'
currout += self.get_group_text(self.byoutput[outdata])
currout += '\n====================================\n'
if count:
currout += 'Count: {0}'.format(len(list(
self.byoutput[outdata])))
currout += '\n====================================\n'
if ismodal:
ismodal = False
currout += outdata
else:
currout += '\n'.join(colordiff(modaloutput.split('\n'),
outdata.split('\n')))
currout += '\n\n'
if reverse:
revoutput.append(currout)
else:
output.write(currout)
for currout in reversed(revoutput):
output.write(currout)
output.flush()
if __name__ == '__main__':
groupoutput = GroupedData()
for line in sys.stdin.read().split('\n'):
if not line:
continue
groupoutput.add_line(*line.split(': ', 1))
groupoutput.print_deviants()
+15 -2
View File
@@ -16,18 +16,31 @@
# limitations under the License.
import confluent.tlv as tlv
from datetime import datetime
import json
import struct
def decodestr(value):
ret = None
try:
ret = value.decode('utf-8')
except UnicodeDecodeError:
try:
ret = value.decode('cp437')
except UnicodeDecodeError:
ret = value
return ret
def unicode_dictvalues(dictdata):
for key in dictdata:
if isinstance(dictdata[key], str):
dictdata[key] = dictdata[key].decode('utf-8')
dictdata[key] = decodestr(dictdata[key])
elif isinstance(dictdata[key], datetime):
dictdata[key] = dictdata[key].strftime('%Y-%m-%dT%H:%M:%S')
elif isinstance(dictdata[key], list):
for i in xrange(len(dictdata[key])):
if isinstance(dictdata[key][i], str):
dictdata[key][i] = dictdata[key][i].decode('utf-8')
dictdata[key][i] = decodestr(dictdata[key][i])
elif isinstance(dictdata[key][i], dict):
unicode_dictvalues(dictdata[key][i])
elif isinstance(dictdata[key], dict):
+1
View File
@@ -0,0 +1 @@
setenv PATH /opt/confluent/bin:$PATH
+37
View File
@@ -1,2 +1,39 @@
PATH=/opt/confluent/bin:$PATH
export PATH
MANPATH=/opt/confluent/share/man:$MANPATH
export MANPATH
# The aliases below are to signify that file globbing is unwelcome at the shell
# this avoids a problem if a user does a noderange like 'n[21-33] and there is a file
# in the directory like 'n3' that causes the parameter to change and target a totally
# different node
# Unfortunately in bourne shell, we cannot reliably ensure a prepended set-f
# and an appended set +f are both run. alias seems to be the only mechanism
# that can intervene before glob expansion, but it lacks power.
# putting it into a function to append is all well and good, *except* that
# if doing something like 'nodepower compute|grep' causes set -f to execute
# in current shell, and the function to be in a subshell and leaves globbing
# disabled in the parent shell. Instead, store the current command in a
# variable and use that to check for misglobbed noderanges, which was the goal
alias nodeattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeattrib'
alias nodebmcreset='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodebmcreset'
alias nodeboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeboot'
alias nodeconfig='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconfig'
alias nodeconsole='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeconsole'
alias nodedefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodedefine'
alias nodeeventlog='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeeventlog'
alias nodefirmware='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodefirmware'
alias nodegroupattrib='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupattrib'
alias nodegroupdefine='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupdefine'
alias nodegroupremove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodegroupremove'
alias nodehealth='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodehealth'
alias nodeidentify='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeidentify'
alias nodeinventory='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeinventory'
alias nodelist='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodelist'
alias nodemedia='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodemedia'
alias nodepower='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodepower'
alias noderemove='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderemove'
alias nodereseat='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodereseat'
alias noderun='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; noderun'
alias nodesensors='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesensors'
alias nodesetboot='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodesetboot'
alias nodeshell='CURRENT_CMDLINE=$(HISTTIMEFORMAT= builtin history 1); export CURRENT_CMDLINE; nodeshell'
+1
View File
@@ -0,0 +1 @@
for i in *.ronn; do echo -n `head -n 1 $i|awk '{print $1}'`; echo " $i"; done > index.txt
+88
View File
@@ -0,0 +1,88 @@
collate(1) -- Organize text input by node
==============================
## SYNOPSIS
`<other command> | collate [-a] [-d] [-w] [-s] [-c] [-r]`
## DESCRIPTION
**collate** takes input in the form of <nodename>: <data> and groups
the output for each <nodename> together, and checks for identical data to further group nodes together.
Output groups are sorted in descending size such that the topmost group is the largest, and the
last output group is the smallest. In the event of equally sized output groups,
the groups are sorted alphanumerically by their header, and each header has
node and group names sorted alphanumerically.
## OPTIONS
* `-a`, `--abbreviate`:
Use confluent to attempt to shorten the noderange. This can help identify
when output differs along a telling group boundary. For example, if
output suggests a large number of nodes are unreachable, abbreviate
showing 'rack1' as being unreachable may make more obvious a possible cause.
* `-d`, `--diff`:
Express all but the most common result group in terms of diff from
the most common result group
* `-w`, `--watch`:
Update results dynamically as data becomes available, rather than
waiting for the command to fully complete.
* `-s`, `--skipcommon`:
Suppress printing of the most common result text group. This is used to
focus on stray output against a well known and expected result.
* `-c`, `--count`:
Print a count of the number of nodes in an output group under the
noderange.
* `-r`, `--reverse`:
Rather than starting with most common to least common, start with
the least common and print the most common last.
## EXAMPLES
* Organizing power state of multiple nodes:
`# nodepower n1-n12 | collate`
`====================================`
`n1,n2,n3,n4,n7,n8,n9,n10,n11,n12`
`====================================`
`on`
` `
`====================================`
`n5,n6`
`====================================`
`off`
* Using diff to detect distinct UEFI configuration
`# pasu n1-n4 show Processors|collate -d -s`
`====================================`
`n3`
`====================================`
`@@`
` Processors.ProcessorPerformanceStates=Enable`
` Processors.C-States=Enable`
` Processors.PackageACPIC-StateLimit=ACPI C3`
`- Processors.C1EnhancedMode=Enable`
`+ Processors.C1EnhancedMode=Disable`
`- Processors.Hyper-Threading=Enable`
`+ Processors.Hyper-Threading=Disable`
` Processors.ExecuteDisableBit=Enable`
` Processors.IntelVirtualizationTechnology=Enable`
` `
`====================================`
`n1`
`====================================`
`@@`
` Processors.ProcessorPerformanceStates=Enable`
` Processors.C-States=Enable`
` Processors.PackageACPIC-StateLimit=ACPI C3`
`- Processors.C1EnhancedMode=Enable`
`+ Processors.C1EnhancedMode=Disable`
` Processors.Hyper-Threading=Enable`
` Processors.ExecuteDisableBit=Enable`
+36
View File
@@ -0,0 +1,36 @@
confetty(8) --- Interactive confluent client
=================================================
## SYNOPSIS
`confetty`
`confetty <confetty command line>`
## DESCRIPTION
**confetty** launches an interactive CLI session to the
confluent service. It provides a filesystem-like
view of the confluent interface. It is intended to
be mostly an aid for developing client software, with
day to day administration generally being easier with
the various function specific commands.
## COMMANDS
The CLI may be navigated by shell commands and some other
commands.
* `cd`:
Change the location within the tree
* `ls`:
List the elements within the current directory/tree
* `show` **ELEMENT**, `cat` **ELEMENT**:
Display the result of reading a specific element (by full or relative path)
* `unset` **ELEMENT** **ATTRIBUTE**
For an element with attributes, request to clear the value of the attribue
* `set` **ELEMENT** **ATTRIBUTE**=**VALUE**
Set the specified attribute to the given value
* `start` **ELEMENT**
Start a console session indicated by **ELEMENT** (e.g. /nodes/n1/console/session)
* `rm` **ELEMENT**
Request removal of an element. (e.g. rm events/hardware/log clears log from a node)
+14
View File
@@ -0,0 +1,14 @@
confluent(8) -- Start the confluent server
=========================================================
## SYNOPSIS
`confluent`
## DESCRIPTION
**confluent** is the name of the server daemon. It is normally run
through the init subsystem rather than executed directly. All confluent
commands connect to confluent daemon. It provides the web interface, debug,
and unix socket connectivity.
@@ -0,0 +1,26 @@
confluentdbutil(8) -- Backup or restore confluent database
=========================================================
## SYNOPSIS
`confluentdbutil [options] <dump|restore> <path>`
## DESCRIPTION
**confluentdbutil** is a utility to export/import the confluent attributes
to/from json files. The path is a directory that holds the json version.
In order to perform restore, the confluent service must not be running. It
is required to indicate how to treat the usernames/passwords are treated in
the json files (password protected, removed from the files, or unprotected).
## OPTIONS
* `-p`, `--password`:
If specified, information such as usernames and passwords will be encrypted
using the given password.
* `-r`, `--redact`:
Indicates to replace usernames and passwords with a dummy string rather
than included.
* `-u`, `--unprotected`:
The keys.json file will include the encryption keys without any protection.
+86
View File
@@ -0,0 +1,86 @@
nodeattrib(8) -- List or change confluent nodes attributes
=========================================================
## SYNOPSIS
`nodeattrib [-b] <noderange> [<nodeattribute>...]`
`nodeattrib <noderange> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodeattrib -c <noderange> <nodeattribute1> <nodeattribute2> ...`
`nodeattrib -e <noderange> <nodeattribute1> <nodeattribute2> ...`
## DESCRIPTION
**nodeattrib** manages the attributes of confluent nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. Attributes can be
straightforward values, or an expression as documented in nodeattribexpressions(5).
For a full list of attributes, run `nodeattrib <node> all` against a node.
If `-c` is specified, this will set the nodeattribute to a null valid.
This is different from setting the value to an empty string.
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.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
* `-c`, `--clear`:
Clear specified nodeattributes
* `-e`, `--environment`:
Set specified attributes based on exported environment variable of matching name. Environment variable names may be lower case or all upper case. Replace . with _ as needed (e.g. info.note may be specified as either $info_note or $INFO_NOTE
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodeattrib n1-n2`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting an attribute of nodes matching a noderange:
`# nodeattrib n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodeattrib n1,n2 hardwaremanagement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: hardwaremanagement.switch: r8e1`
`n1: hardwaremanagement.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: hardwaremanagement.switch: r8e1`
`n2: hardwaremanagement.switchport: 2`
* Listing matching nodes of a simple noderange that are set:
`# nodeattrib n1-n2 current`
`n1: console.method: ipmi`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: ipmi`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Change attribute on nodes of a simple noderange:
`# nodeattrib n1-n2 console.method=serial`
`n1: console.method: serial`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: console.method: serial`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Clear attribute on nodes of a simple noderange, if you want to retain the variable set the attribute to "":
`# nodeattrib n1-n2 -c console.method`
`# nodeattrib n1-n2 console.method`
`n1: console.method: `
`n2: console.method: `
## SEE ALSO
nodegroupattrib(8), nodeattribexpressions(5)
@@ -0,0 +1,64 @@
nodeattribexpressions(5) -- Confluent attribute expression syntax
=================================================================
## DESCRIPTION
In confluent, any attribute may either be a straightforward value, or an
expression to generate the value.
An expression will contain some directives wrapped in `{}` characters. Within
`{}` are a number of potential substitute values and operations.
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.
Additionally, {n0} is special as representing the last number in a name, so in
the b1o2r3u4 example, {n0} would be 4.
Frequently a value derives from a number in the node name, but must undergo a
transform to be useful. As an example, if we have a scheme where nodes are
numbered n1-n512, and they are arranged 1-42 in rack1, 43-84 in rack2, and so
forth, it is convenient to perform arithmetic on the extracted number. Here is
an example of codifying the above scheme, and setting the u to the remainder:
`location.rack=rack{(n1-1)/42+1}`
`location.u={(n1-1)%42+1}`
Note how text may be mixed into expressions, only data within {} will receive
special treatment. Here we also had to adjust by subtracting 1 and adding it
back to make the math work as expected.
It is sometimes the case that the number must be formatted a different way,
either specifying 0 padding or converting to hexadecimal. This can be done by a
number of operators at the end to indicate formatting changes.
`{n1:02x} - Zero pad to two decimal places, and convert to hexadecimal, as mightbe used for generating MAC addresses`
`{n1:x} - Hexadecimal without padding, as may be used in a generated IPv6 address`
`{n1:X} - Uppercase hexadecimal`
`{n1:02d} - Zero pad a normal numeric representation of the number.`
Another common element to pull into an expression is the node name in whole:
`hardwaremanagement.manager={node}-imm`
Additionally other attributes may be pulled in:
`hardwaremanagement.switchport={location.u}`
Multiple expressions are permissible within a single attribute:
`hardwaremanagement.manager={node}-{hardwaremanagement.method}`
A note to developers: in general the API layer will automatically recognize a
generic set attribute to string with expression syntax and import it as an
expression. For example, submitting the following JSON:
`{ 'location.rack': '{n1}' }`
Will auto-detect {n1} as an expression and assign it normally. If wanting to
set that value verbatim, it can either be escaped by doubling the {} or by
explicitly declaring it as a value:
`{ 'location.rack': '{{n1}}' }`
`{ 'location.rack': { 'value': '{n1}' } }`
@@ -0,0 +1,19 @@
nodebmcreset(8) -- Reset management controller
=========================================================
## SYNOPSIS
`nodebmcreset <noderange>`
## DESCRIPTION
`nodebmcreset` allows you to reset the management controller of the specified noderange
## EXAMPLES:
* Reset the management controller for nodes n1 through n4:
`# nodebmcreset n1-n4`
`n1: BMC Reset Successful`
`n2: BMC Reset Successful`
`n3: BMC Reset Successful`
`n4: BMC Reset Successful`
+34
View File
@@ -0,0 +1,34 @@
nodeboot(8) -- Reboot a confluent node to a specific device
=========================================================
## SYNOPSIS
`nodeboot <noderange>`
`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.
## EXAMPLES
* Booting n3 and n4 to the default boot behavior:
`# nodeboot n3-n4`
`n3: default`
`n4: default`
`n3: on->reset`
`n4: on->reset`
* Booting n1 and n2 to setup menu:
`# nodeboot n1-n2 setup`
`n2: setup`
`n1: setup`
`n2: on->reset`
`n1: on->reset`
* Booting n3 and n4 to network:
`# nodeboot n3-n4 net`
`n3: network`
`n4: network`
`n4: on->reset`
`n3: off->on`
+50
View File
@@ -0,0 +1,50 @@
nodeconfig(8) -- Show or change node configuration
==================================================
## SYNOPSIS
`nodeconfig <noderange> [options] [<configuration>..]`
`nodeconfig <noderange> [options] [<configuration=value>..]`
## DESCRIPTION
**nodeconfig** manages the configuration of nodes managed by confluent.
Rather than manipulating the confluent database, this actually modifies the
running configuration on the node firmware. Calling without '=' will show the
current value, and '=' will change the value. Network information can be
given as a node expression, as documented in the man page for nodeattribexpressions(5).
## OPTIONS
* `-c`, `--comparedefault`:
Take the given settings and compare against default value, if available. If
no configuration values are specified, it will show only those that differ.
If combined with `-x`, will show all differing values except those indicated
by `-x`
* `-x`, `--exclude`:
Rather than listing only the specified configuration parameters, list all
attributes except for the specified ones
* `-d`, `--detail`:
Provide detailed data as available. This can include help text and valid
values for a setting.
## EXAMPLES
* Showing the current IP configuration of noderange BMC/IMM/XCC:
`# nodeconfig s3,s4 bmc`
`s3: bmc.ipv4_address: 172.30.254.193/16`
`s3: bmc.ipv4_method: DHCP`
`s3: bmc.ipv4_gateway: 172.30.0.6`
`s4: bmc.ipv4_address: 172.30.254.192/16`
`s4: bmc.ipv4_method: DHCP`
`s4: bmc.ipv4_gateway: 172.30.0.6`
* Changing nodes `s3` and `s4` to have the ip addressess 10.1.2.3 and 10.1.2.4 with a 16 bit subnet mask:
`# nodeconfig s3,s4 bmc.ipv4_address=10.1.2.{n1}/16`
## SEE ALSO
nodeattribexpressions(5)
+57
View File
@@ -0,0 +1,57 @@
nodeconsole(8) -- Open a console to a confluent node
=====================================================
## SYNOPSIS
`nodeconsole <node>`
## DESCRIPTION
**nodeconsole** opens an interactive console session to a given node. This is the
text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `c`,
then `.`. Note that console output by default is additionally logged to
`/var/log/confluent/consoles/`**NODENAME**.
When the console connection to the target is broken, then confluent on backend
will initiate an automatic retry interval that is randomized between 2 and 4 minutes.
The reopen escape sequence below requests an immediate retry, as does connecting
a new session.
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
sequences. To begin an command escape sequence, hit `Ctrl-e`, then `c`. The next
keystroke will be interpreted as a command. The following commands are available.
* `.`:
Exit the session and return to the command prompt
* `b`:
[send Break]
Send a break to the remote console when possible (some console plugins may not support this)
* `o`:
[reOpen]
Request confluent to disconnect and reconnect to console. For example if there is suspicion
that the console has gone inoperable, but would work if reconnected.
* `po`:
[Power Off]
Power off server immediately, without waiting for OS to shutdown
* `ps`:
[Power Shutdown]
Request OS shut down gracefully, and then power off
* `pb<ent>`:
[Power Boot]
Cause system to immediately boot, resetting or turning on as appropriate.
Hitting enter is required to execute the reboot rather than another pb sequence
* `pbs`:
[Power Boot Setup]
Request immediate boot ultimately landing in interactive firmware setup
* `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>`:
Hit enter to skip entering a command at the escape prompt.
+28
View File
@@ -0,0 +1,28 @@
nodedefine(8) -- Define new confluent nodes
## SYNOPSIS
`nodedefine <noderange> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodedefine` allows the definition of new nodes for the confluent management
system. It has the same syntax as `nodeattrib(8)`, and the commands differ in
that `nodeattrib(8)` will error if a node does not exist.
## EXAMPLES
* Define two racks of nodes, named r{rack}u{u}:
`# nodedefine r1u1-r2u4`
`r1u4: created`
`r1u1: created`
`r1u2: created`
`r1u3: created`
`r2u4: created`
`r2u3: created`
`r2u2: created`
`r2u1: created`
## SEE ALSO
noderange(5), nodeattribexpressions(8)
+130
View File
@@ -0,0 +1,130 @@
nodediscover(8) -- List or manage confluent node discovery
=========================================================
## SYNOPSIS
`nodediscover rescan`
`nodediscover [options] list`
`nodediscover [options] assign`
`nodediscover [options] rescan`
`nodediscover [options] clear`
## DESCRIPTION
**nodediscover** provides streamlined access to the confluent discovery data
and assignment. Nodes are detected through either an active scan (as occurs
at service startup and on request by nodediscover rescan) or through passive
detection (as a target comes online, it may attempt to register with the
network).
**nodediscover list** provides the currently known data in tabular format. The
data may be filtered by various parameters, as denoted in the options below.
**nodediscover assign** performs manual discovery, assigning an entry to a node
identity or, using `-i`, using a csv file to assign nodes all at once. For
example, a spreadsheet of serial numbers to desired node names could be used.
**nodediscover rescan** requests the server to do an active sweep for new
devices. Generally every effort is made to passively detect devices as they
become available (as they boot or are plugged in), however sometimes an active
scan is the best approach to catch something that appears to be missing.
**nodedsicover clear** requests the server forget about a selection of
detected device. It takes the same arguments as **nodediscover list**.
## CSV FORMAT
The CSV format used by nodediscover consists of one header describing the
columns followed by the data. The available columns are:
* node: The name desired for the node in confluent
* groups: A comma delimited list of groups to put the node into (using normal CSV escape rules for the commas)
* mac: The mac address of the node
* serial: The serial number of the node
* uuid: The uuid of the node
* bmc: The name or ip address that should be assigned to the BMC, regardless of current address
* bmc_gateway: IP address of gateway, if desired
* bmcuser: The desired username for the BMC to have as administrator
* bmcpass: The desired password for the BMC to require
Note that node is the only mandatory field. To identify the systems, one of
mac, serial, or uuid should be specified, it is pointless to provide more than
one of these columns. Other attributes if not provided may be defined through
nodeattrib or group inherited. It is possible to define nodes without ever
providing a BMC ip, in which case IPv6 will be used automatically if possible.
One example of a valid CSV file would be:
node,serial,bmc,bmcuser,bmcpass
n1,06DPMDF,172.30.204.1,admin,Passw0rd12
n2,J30002HG,172.30.204.2,admin,Passw0rd12
Which would use the serial number to assign the name and other three values to
the nodes.
## OPTIONS
* `-m MODEL`, `--model=MODEL`:
Operate with nodes matching the specified model number
* `-s SERIAL`, `--serial=SERIAL`:
Operate against the system matching the specified
serial number
* `-u UUID`, `--uuid=UUID`:
Operate against the system matching the specified UUID
* `-n NODE`, `--node=NODE`:
Operate with the given nodename
* `-e MAC`, `--ethaddr=MAC`:
Operate against the system with the specified MAC
address
* `-t TYPE`, `--type=TYPE`:
Operate against the system of the specified type
* `-c`, `--csv`:
Use CSV formatted output
* `-i IMPORT.CSV`, `--import=IMPORT.CSV`:
Import bulk assignment data from given CSV file
* `-d STATE`, `--discoverystate=STATE`:
Indicate devices with a particular state. The states are listed below
* discovered: The device has been identified and has also had discovery
activities performed, including any relevant certificate
exchanges and deploying user and network configuration.
* identified: The device has been identified as to what node it is
supposed to be, however no active changes to the attributes
or device configuration has been performed. This is
generally discovery awaiting approval due to
discovery.policy specifying a strict security model.
* unidentified: A device has been sensed, but no node identity has been
established at all. It provides data that can be used
for nodediscover assign, as well as current IP addresses
that can be used for manual efforts as possible.
## EXAMPLES
* Listing all detected Lenovo IMMv2 systems on a local network:
`# nodediscover list -t lenovo-imm2`
` Node| Model| Serial| UUID| Mac Address| Type| Current IP Addresses`
`---------------|---------------|---------------|------------------------------------|-----------------|------------|------------------------------------------------`
` r2| 5463AC1| 06DPMDF|5f7133b8-c8cb-11e4-99a9-40f2e9b91018|40:f2:e9:b9:10:1d| lenovo-imm2| 172.30.204.1,fe80::42f2:e9ff:feb9:101d%eth1`
` | 7906AC1| 06PBX15|e98d483d-2759-11e1-8ffd-5cf3fc11249c|5c:f3:fc:11:24:9f| lenovo-imm2| 172.30.3.12,fe80::5ef3:fcff:fe11:249f%eth1`
` n1| 8737AC1| 23XXH41|14dd3ba6-5c38-11e1-931a-5cf3fc6e4680|5c:f3:fc:6e:13:e1| lenovo-imm2| 172.30.3.1,fe80::5ef3:fcff:fe6e:13e1%eth1`
` n7| 8737AC1| 23XXH32|79d2ce28-5cd5-11e1-8c86-5cf3fc6e46b0|5c:f3:fc:6e:13:f9| lenovo-imm2| 172.30.3.7,fe80::5ef3:fcff:fe6e:13f9%eth1`
` n8| 8737AC1| 23XXH49|551a8438-5cd5-11e1-8d6c-5cf3fc6e4708|5c:f3:fc:6e:14:25| lenovo-imm2| 172.30.3.8,fe80::5ef3:fcff:fe6e:1425%eth1`
` n3| 8737AC1| 23XXH30|1dd7f7b3-5da5-11e1-baf0-5cf3fc6e4738|5c:f3:fc:6e:14:3d| lenovo-imm2| 172.30.3.3,fe80::5ef3:fcff:fe6e:143d%eth1`
` n4| 8737AC1| 23XXH35|45b81dae-5d9b-11e1-8337-5cf3fc6e4858|5c:f3:fc:6e:14:cd| lenovo-imm2| 172.30.3.4,fe80::5ef3:fcff:fe6e:14cd%eth1`
` n11| 8737AC1| 23XXH12|31d90128-5c37-11e1-bdb7-5cf3fc6e4920|5c:f3:fc:6e:15:31| lenovo-imm2| 172.30.3.11,fe80::5ef3:fcff:fe6e:1531%eth1`
` n13| 8737AC1| 23XXH44|e23a138a-5cd3-11e1-8f3d-5cf3fc6e4950|5c:f3:fc:6e:15:49| lenovo-imm2| 172.30.3.13,fe80::5ef3:fcff:fe6e:1549%eth1`
` | 8737AC1| 23XXH29|5cd1216b-5c37-11e1-ba0c-5cf3fc6e49c8|5c:f3:fc:6e:15:85| lenovo-imm2| 172.30.3.9,fe80::5ef3:fcff:fe6e:1585%eth1`
` | 8737AC1| 23ZYT44|f4bf48ca-71f0-11e1-b274-5cf3fc6e4f10|5c:f3:fc:6e:18:29| lenovo-imm2| 172.30.3.10,fe80::5ef3:fcff:fe6e:1829%eth1`
` hpcedr| 7915AC1| 06DRHL5|a64e3014-d7e3-11e1-8d21-6cae8b1dff32|6c:ae:8b:1d:ff:36| lenovo-imm2| 172.30.254.250,fe80::6eae:8bff:fe1d:ff36%eth1`
` | 8737AC1| 06YRWC3|3af85a51-7efd-11e3-8599-000af7482e00|6c:ae:8b:32:cb:c5| lenovo-imm2| 172.30.3.6,fe80::6eae:8bff:fe32:cbc5%eth1`
` | 8737AC1| 06YRWB7|b230f62e-7efd-11e3-9773-000af7482980|6c:ae:8b:32:cd:01| lenovo-imm2| 172.30.3.5,fe80::6eae:8bff:fe32:cd01%eth1`
` | 8737AC1| 06YRWC7|09586005-7efe-11e3-9f03-000af7482df0|6c:ae:8b:32:cd:a5| lenovo-imm2| 172.30.3.2,fe80::6eae:8bff:fe32:cda5%eth1`
* Manually assign a single node according to serial number:
`[root@odin ~]# nodediscover assign -s 06PBX15 -n n12`
`Assigned: n12`
* Bulk execute discovery based on spreadsheet:
`[root@odin ~]# nodediscover assign -i import.csv`
`Defined r2`
`Discovered r2`
`Defined c1`
`Discovered c1`
@@ -0,0 +1,33 @@
nodeeventlog(8) -- Pull eventlog from confluent nodes
============================================================
## SYNOPSIS
`nodeeventlog <noderange>`
`nodeeventlog <noderange> [clear]`
## DESCRIPTION
`nodeeventlog` pulls and optionally clears the event log from the requested
noderange.
## EXAMPLES
* Pull the event log from n2 and n3:
`# nodeeventlog n2,n3`
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
`n2: 05/03/2017 11:44:56 System Firmware - Progress - Unspecified`
`n3: 05/03/2017 11:44:39 Event Log Disabled - SEL Fullness - Log clear`
`n3: 05/03/2017 11:45:00 System Firmware - Progress - Unspecified`
`n3: 05/03/2017 11:47:22 System Firmware - Progress - Starting OS boot`
* Pull and clear the event log from n2 and n3:
`# nodeeventlog n2,n3 clear`
`n2: 05/03/2017 11:44:25 Event Log Disabled - SEL Fullness - Log clear`
`n2: 05/03/2017 11:44:56 System Firmware - Progress - Unspecified`
`n2: 05/03/2017 11:48:29 System Firmware - Progress - Starting OS boot`
`n3: 05/03/2017 11:44:39 Event Log Disabled - SEL Fullness - Log clear`
`n3: 05/03/2017 11:45:00 System Firmware - Progress - Unspecified`
`n3: 05/03/2017 11:47:22 System Firmware - Progress - Starting OS boot`
`# nodeeventlog n2,n3`
`n2: 05/03/2017 11:48:48 Event Log Disabled - SEL Fullness - Log clear`
`n3: 05/03/2017 11:48:52 Event Log Disabled - SEL Fullness - Log clear`
@@ -0,0 +1,43 @@
nodefirmware(8) -- Report firmware information on confluent nodes
=================================================================
## SYNOPSIS
`nodefirmware <noderange>`
`nodefirmware <noderange> <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.
## EXAMPLES
* Pull firmware from a node:
`# nodefirmware r1`
`r1: IMM: 3.70 (TCOO26H 2016-11-29T05:09:51)`
`r1: IMM Backup: 1.71 (TCOO10D 2015-04-17T00:00:00)`
`r1: IMM Trusted Image: TCOO26H`
`r1: UEFI: 2.31 (TCE128I 2016-12-13T00:00:00)`
`r1: UEFI Backup: 2.20 (TCE126O)`
`r1: FPGA: 3.2.0`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller Bootcode: 1.38`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MBA: 16.8.0`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller Firmware Package: 0.0.0a`
`r1: ServeRAID M1215 MegaRAID Controller Firmware: 24.12.0-0038 (2016-10-20T00:00:00)`
`r1: ServeRAID M1215 Disk 28 MBF2600RC: SB2C`
`r1: ServeRAID M1215 Disk 29 MBF2600RC: SB2C`
`r1: ServeRAID M5210 Disk 0 MBF2600RC: SB2C`
`r1: ServeRAID M5210 Disk 1 MBF2600RC: SB2C`
`r1: ServeRAID M5210 Disk 2 MBF2600RC: SB2C`
@@ -0,0 +1,42 @@
nodegroupattrib(8) -- List or change confluent nodegroup attributes
===================================================================
## SYNOPSIS
`nodegroupattrib <group> [ current | all ]`
`nodegroupattrib <group> [<nodeattribute>...]`
`nodegroupattrib <group> [<nodeattribute1=value1> <nodeattribute2=value2> ...]`
`nodegroupattrib <group> [-c] [<nodeattribute1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodegroupattrip` queries the confluent server to get information about nodes.
In the simplest form, it simply takes the given group and lists the attributes of that group.
Contrasted with nodeattrib(8), settings managed by nodegroupattrib will be added
and removed from a node as it is added or removed from a group. If an attribute
is set using nodeattrib(8) against a noderange(5) that happens to be a group name,
nodeattrib(8) individually sets attributes directly on each individual node that is
currently a member of that group. Removing group membership or adding a new
node after using the nodeattrib(8) command will not have attributes change automatically.
It's easiest to see by using the `nodeattrib <noderange> -b` to understand how
the attributes are set on the node versus a group to which a node belongs.
## OPTIONS
* `-c`, `--clear`:
Clear specified nodeattributes.
## EXAMPLES
* Show attributes of a group called `demogrp`:
`# nodegroupattrib demogrp`
`demogrp: hardwaremanagement.manager: (will derive from expression 10.30.{n0/255}.{n0%255})`
`demogrp: nodes: n12,n13,n10,n11,n9,n1,n2,n3,n4`
* Set location.u to be the remainder of first number in node name when divided by 42:
`# nodegroupattrib demogrp location.u={n1%42}`
## SEE ALSO
nodeattrib(8), nodeattribexpressions(5)
@@ -0,0 +1,23 @@
nodegroupdefine(8) -- Define new confluent node group
## SYNOPSIS
`nodegroupdefine <groupname> [nodeattribute1=value1> <nodeattribute2=value2> ...]`
## DESCRIPTION
`nodegroupdefine` allows the definition of a new node for the confluent management
service. It may only define a single group name at a time.
It has the same syntax as `nodegroupattrib(8)`, and the commands differ in
that `nodegroupattrib(8)` will error if a node group does not exist.
## EXAMPLES
* Create a group called `compute`:
`# nodegroupdefine compute`
`compute: created`
## SEE ALSO
nodeattribexpressions(8), nodegroupattrib(8), nodegroupremove(8)
@@ -0,0 +1,18 @@
nodegroupremove(8) -- Remove a nodegroup from the confluent database
====================================================================
## SYNOPSIS
`nodegroupremove <noderange>`
## DESCRIPTION
`nodegroupremove` simply removes the given single nodegroup from the confluent database.
## EXAMPLES
* Remove group called testgroup
`# nodegroupremove testgroup`
`testgroup: deleted`
+22
View File
@@ -0,0 +1,22 @@
nodehealth(8) -- Show health summary of confluent nodes
========================================================
## SYNOPSIS
`nodehealth <noderange>`
## DESCRIPTION
`nodehealth` reports the current health assessment of a confluent node. It
will report either `ok`, `warning`, `critical`, or `failed`, along with
a string explaining the reason for any result other than `ok`.
## EXAMPLES
* Pull health summary of 5 nodes:
`# nodehealth n1-n4,r1`
`n1: critical (Mezz Exp 2 Fault:Critical)`
`n3: ok`
`n2: ok`
`r1: ok`
`n4: ok`
@@ -0,0 +1,31 @@
nodeidentify(8) -- Control the identify LED of confluent nodes
=========================================================
## SYNOPSIS
`nodidentify <noderange> [on|off]`
## DESCRIPTION
`nodeidentify` allows you to turn on or off the location LED of conflueunt nodes,
making it easier to determine the physical location of the nodes. The following
options are supported:
* `on`: Turn on the identify LED
* `off`: Turn off the identify LED
## EXAMPLES:
* Turn on the identify LED on nodes n1 through n4:
`# nodeidentify n1-n4 on`
`n1: on`
`n2: on`
`n3: on`
`n4: on`
* Turn off the identify LED on nodes n1 thorugh n4:
`# nodeidentify n1-n4 off`
`n1: off`
`n2: off`
`n4: off`
`n3: off`
+121
View File
@@ -0,0 +1,121 @@
nodeinventory(8) -- Get hardware inventory of confluent node
===============================================================
## SYNOPSIS
`nodeinventory <noderange> [serial|model|uuid|mac]`
## DESCRIPTION
`nodeinventory` pulls information about hardware of a node. This includes
information such as adapters, serial numbers, processors, and memory modules,
as supported by the platforms hardware management implementation. It accepts
arguments such as serial or model or others as listed above to filter
output to specific data.
## EXAMPLES
* Pulling inventory of a node named r1:
`# nodeinventory r1`
`r1: System MAC Address 1: 40:f2:e9:af:45:a0`
`r1: System MAC Address 2: 40:f2:e9:af:45:a1`
`r1: System MAC Address 3: 40:f2:e9:af:45:a2`
`r1: System MAC Address 4: 40:f2:e9:af:45:a3`
`r1: System Board manufacturer: IBM`
`r1: System Product name: System x3650 M5`
`r1: System Device ID: 32`
`r1: System Revision: 9`
`r1: System Product ID: 323`
`r1: System Board model: 00KG915`
`r1: System Device Revision: 0`
`r1: System Serial Number: E2K4831`
`r1: System Board manufacture date: 2014-10-20T12:00`
`r1: System Board serial number: Y010UF4AL0B5`
`r1: System Manufacturer: IBM`
`r1: System FRU Number: 00FK639`
`r1: System Board product name: System Board`
`r1: System Model: 5462AC1`
`r1: System UUID: 1B29CE46-765E-31A3-A3B9-B5FB934F15AB`
`r1: System Hardware Version: 0x0000`
`r1: System Manufacturer ID: 20301`
`r1: System Chassis serial number: E2K4831`
`r1: System Asset Number: `
`r1: System Chassis type: Other`
`r1: Power Supply 1 Board model: 94Y8136`
`r1: Power Supply 1 Board manufacturer: EMER`
`r1: Power Supply 1 FRU Number: 94Y8137`
`r1: Power Supply 1 Board product name: IBM Designed Device`
`r1: Power Supply 1 Board manufacture date: 2014-11-08T00:00`
`r1: Power Supply 1 Board serial number: K13814B88ED`
`r1: Power Supply 1 Revision: 49`
`r1: Power Supply 2: Not Present`
`r1: DASD Backplane 1 Board model: 00JY139`
`r1: DASD Backplane 1 Board manufacturer: WIST`
`r1: DASD Backplane 1 FRU Number: 00FJ756`
`r1: DASD Backplane 1 Board product name: IBM Designed Device`
`r1: DASD Backplane 1 Board manufacture date: 2014-08-28T00:00`
`r1: DASD Backplane 1 Board serial number: Y011UF48W02U`
`r1: DASD Backplane 1 Revision: 0`
`r1: DASD Backplane 2: Not Present`
`r1: DASD Backplane 3: Not Present`
`r1: DASD Backplane 4: Not Present`
`r1: DASD Backplane 5 Board model: 00YJ530`
`r1: DASD Backplane 5 Board manufacturer: WIST`
`r1: DASD Backplane 5 FRU Number: 00AL953`
`r1: DASD Backplane 5 Board product name: IBM Designed Device`
`r1: DASD Backplane 5 Board manufacture date: 2016-02-04T00:00`
`r1: DASD Backplane 5 Board serial number: Y010UF624024`
`r1: DASD Backplane 5 Revision: 0`
`r1: DASD Backplane 6: Not Present`
`r1: CPU 1 Hardware Version: Intel(R) Xeon(R) CPU E5-2640 v3 @ 2.60GHz`
`r1: CPU 1 Asset Number: Unknown`
`r1: CPU 1 Manufacturer: Intel(R) Corporation`
`r1: CPU 2: Not Present`
`r1: ML2 Card: Not Present`
`r1: DIMM 1: Not Present`
`r1: DIMM 2: Not Present`
`r1: DIMM 3: Not Present`
`r1: DIMM 4: Not Present`
`r1: DIMM 5: Not Present`
`r1: DIMM 6: Not Present`
`r1: DIMM 7: Not Present`
`r1: DIMM 8: Not Present`
`r1: DIMM 9: Not Present`
`r1: DIMM 10: Not Present`
`r1: DIMM 11: Not Present`
`r1: DIMM 12: Not Present`
`r1: DIMM 13: Not Present`
`r1: DIMM 14: Not Present`
`r1: DIMM 15: Not Present`
`r1: DIMM 16: Not Present`
`r1: DIMM 17: Not Present`
`r1: DIMM 18: Not Present`
`r1: DIMM 19: Not Present`
`r1: DIMM 20: Not Present`
`r1: DIMM 21: Not Present`
`r1: DIMM 22: Not Present`
`r1: DIMM 23: Not Present`
`r1: DIMM 24: Not Present`
`r1: X8 PCI 1: Not Present`
`r1: X8 PCI 2: Not Present`
`r1: X8 PCI 6: Not Present`
`r1: X8 PCI 7: Not Present`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 1: 40:f2:e9:af:45:a0`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 2: 40:f2:e9:af:45:a1`
`r1: Broadcom NetXtreme Gigabit Ethernet Controller MAC Address 3: 40:f2:e9:af:45:a2`
`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`
+47
View File
@@ -0,0 +1,47 @@
nodelist(8) -- List confluent nodes and their attributes
=========================================================
## SYNOPSIS
`nodelist <noderange>`
`nodelist <noderange> [-b] <nodeattribute>...`
## DESCRIPTION
**nodelist** queries the confluent server to get information about nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. There is more
information on node attributes in nodeattributes(5) man page.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodelist n1-n4`
`n1`
`n2`
`n3`
`n4`
* Getting an attribute of nodes matching a noderange:
`# nodelist n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodelist n1,n2 hardwaremanegement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: hardwaremanagement.switch: r8e1`
`n1: hardwaremanagement.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: hardwaremanagement.switch: r8e1`
`n2: hardwaremanagement.switchport: 2`
+56
View File
@@ -0,0 +1,56 @@
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.
`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)`
+43
View File
@@ -0,0 +1,43 @@
nodepower(8) -- Check or change power state of confluent nodes
=========================================================
## SYNOPSIS
`nodepower <noderange>`
`nodepower <noderange> [on|off|boot|shutdown|reset|status]`
## DESCRIPTION
**nodepower** with only a noderange will retrieve current power state of nodes
through confluent. When given an additional argument, it will request a change
to the power state of the nodes. The following arguments are recognized:
* `on`: Turn on the specified noderange. Nothing will happen to nodes of
the noderange that are already on.
* `off`: Immediately turn off the specified noderange, without waiting for OS
to shutdown. Nothing will happen to nodes of the noderange that are already on.
* `boot`: Immediately boot a system. This will power on nodes of the noderange
that are off, and reset nodes of the noderange that are on. The previous state
will be reflected in the output.
* `shutdown`: Request the OS gracefully shut down. Nothing will happen for
nodes that are off, and nodes will not shutdown if the OS fails to gracefully
respond.
* `reset`: Request immediate reset of nodes of the noderange. Nodes that are
off will not react to this request.
* `status`: Behave identically to having no argument passed at all.
## EXAMPLES
* Get power state of nodes n1 through n4:
`# nodepower n1-n4`
`n1: on`
`n2: on`
`n3: on`
`n4: off`
* Forcing a reboot of nodes n1-n4:
`# nodepower n1-n4 boot`
`n3: on->reset`
`n1: on->reset`
`n2: on->reset`
`n4: off->on`
+57
View File
@@ -0,0 +1,57 @@
noderange(5) -- Indicate a target set of nodes in confluent
=========================================================
## SYNOPSIS
`<noderange>`
## DESCRIPTION
<noderange> is a syntax that can be used in most Confluent commands to conveniently specify a list of nodes. The result is that the command will be applied to a range of nodes, often in parallel.
## EXAMPLES:
The simplest noderange is a single node name:
`n1`
Nodes and groups may be used interchangeably. The following may be used in any context where n1 would be accepted as a noderange:
`rack1`
Commonly, there is a desire to target a range of elements. There are a few identically behaving syntaxes for the purpose.
`n1:n20`
`n1-n20`
`n[1-20]`
Note that numbers may be zero padded or not, it will automatically detect the padding amount and adjust members of the range accordingly. Ranges also can understand multiple numeric values changing:
`r[1-3]u[01-10]`
`r1u01-r3u10`
Ranges can also be applied to group names, and all above syntaxes are compatible:
`rack1-rack10`
`rack[1-10]`
Also, regular expressions may be used to indicate nodes with names matching certain patterns:
`~r1u..`
The other major noderange primitive is indicating nodes by some attribute value:
`location.rack=7`
Commas can be used to indicate multiple nodes, and can mix and match any of the above primitives. The following can be a valid single noderange, combining any and all members of each comma separated component
`n1,n2,rack1,storage,location.rack=9,~s1..,n20-n30`
Exclusions can be done by prepending a ‘-‘ before a portion of a noderange:
`rack1,-n2`
`compute,-rack1`
`compute,-location.row=12`
To indicate nodes that match multiple selections at once (set intersection), the @ symbol may be used:
`compute@rack1`
`location.rack=10@compute`
For complex expressions, () may be used to indicate order of expanding the noderange to be explicit
`rack1,-(console.logging=full@compute)`
Noderange syntax can also indicate ‘pagination’, or separating the nodes into well defined chunks. > is used to indicate how many nodes to display at a time, and < is used to indicate how many nodes to skip into a noderange:
`rack1>3<6`
The above would show the seventh through ninth nodes of the rack1 group. Like all other noderange operations, this may be combined with any of the above, but must appear as the very last operation. Ordering is done with a natural sort.
+25
View File
@@ -0,0 +1,25 @@
noderemove(8) -- Remove nodes from the confluent management service
===================================================================
## SYNOPSIS
`noderemove <noderange>`
## DESCRIPTION
`noderemove` simply removes the given noderange from the confluent database.
## EXAMPLES
* Remove two racks each with 4 nodes:
`# noderemove r1u1-r2u4`
`r1u4: deleted`
`r1u1: deleted`
`r1u2: deleted`
`r1u3: deleted`
`r2u4: deleted`
`r2u3: deleted`
`r2u2: deleted`
`r2u1: deleted`
+19
View File
@@ -0,0 +1,19 @@
nodereseat(8) -- Request a reseat of a node
============================================
## SYNOPSIS
`nodereseat <noderange>`
## DESCRIPTION
`nodereseat` requests the enclosure manager of the current node to reseat that
node's slot. This should be equivalent to removing the system entirely from
the chassis and putting it back in, but without actually having to do so.
## EXAMPLES
* Reseating the node `s1`:
`# nodereseat s1`
`s1: Reseat successful`
+53
View File
@@ -0,0 +1,53 @@
noderun(8) -- Run arbitrary commands per node in a noderange
=============================================================
## SYNOPSIS
`noderun <noderange> <command string>`
## DESCRIPTION
`noderun` will take a given command and execute it in parallel once per node
in the specified noderange. Attribute expressions as documented in
nodeattribexpressions(5) are expanded prior to execution of the command. For
noderun, the commands are locally executed. To execute commands on the nodes
themselves, see nodeshell(8).
## EXAMPLES
* Run ping against nodes n1 through n4:
`# noderun n1-n4 ping -c 1 {nodename}`
`n3: PING n3 (172.30.2.3) 56(84) bytes of data.`
`n3: 64 bytes from n3 (172.30.2.3): icmp_seq=1 ttl=64 time=0.387 ms`
`n3: `
`n3: --- n3 ping statistics ---`
`n3: 1 packets transmitted, 1 received, 0% packet loss, time 0ms`
`n3: rtt min/avg/max/mdev = 0.387/0.387/0.387/0.000 ms`
`n4: PING n4 (172.30.2.4) 56(84) bytes of data.`
`n4: 64 bytes from n4 (172.30.2.4): icmp_seq=1 ttl=64 time=0.325 ms`
`n4: `
`n4: --- n4 ping statistics ---`
`n4: 1 packets transmitted, 1 received, 0% packet loss, time 0ms`
`n4: rtt min/avg/max/mdev = 0.325/0.325/0.325/0.000 ms`
`n2: PING n2 (172.30.2.2) 56(84) bytes of data.`
`n2: From odin (172.30.0.6) icmp_seq=1 Destination Host Unreachable`
`n2: `
`n2: --- n2 ping statistics ---`
`n2: 1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 3000ms`
`n2: `
`n1: PING n1 (172.30.2.1) 56(84) bytes of data.`
`n1: `
`n1: --- n1 ping statistics ---`
`n1: 1 packets transmitted, 0 received, 100% packet loss, time 10000ms`
`n1: `
* Run an ipmitool raw command against the management controllers of n1 through n4:
`# noderun n1-n4 ipmitool -I lanplus -U USERID -E -H {hardwaremanagement.manager} raw 0 1`
`n3: 01 10 00`
`n1: 01 10 00`
`n4: 01 10 00`
`n2: 01 10 00`
## SEE ALSO
nodeshell(8)
+55
View File
@@ -0,0 +1,55 @@
nodesensors(8) --- Retrieve telemetry for sensors of confluent nodes
====================================================================
## SYNOPSIS
`nodesensors <noderange> [-c] [-i <interval>] [-n <samplecount>] [<sensor name or category>...]`
## DESCRIPTION
**nodesensors** queries the confluent server to get telemetry from nodes. Telemetry can include
data such as temperature, power, and so forth. Without arguments, it lists all available sensors
and their current values. If `-c` is specified, CSV format is used for output. Normally
nodesensors outputs once and exits. Repeated periodic gathering can be done with `-i` to specify
interval and `-n` to specify number of requests. If `-i` is specified without `-n`, then it will
retrieve data at the requested interval indefinitely. If '-n' is specified without `-i`, an
interval of 1 second is used.
## OPTIONS
* `-c`, `--csv`:
Organize output into CSV format, one sensor per column.
* `-i`, `--interval`=**SECONDS**:
Repeat data gathering waiting, waiting the specified time between samples. Unless `-n` is
specified, indefinite retrieval is assumed.
* `-n`, `--numreadings`=**SAMPLES**:
Perform the specified number of readings, waiting `-i` indicated interval or 1 second if not
otherwise indicated.
## EXAMPLES
* Retrieving all temperature related sensors from one system
`# nodesensors n1 temperature`
`n1: CPU 1 Overtemp: Ok`
`n1: CPU 2 Overtemp: Ok`
`n1: Inlet Temp: 16.0 °C`
`n1: PCH Overtemp: Ok`
`n1: LOM Temp: Ok`
* Retrieving a sensor named "Inlet Temp" for 4 systems over a 3 second period of time:
`# nodesensors n1-n4 'Inlet Temp' -c -n 3`
`time,node,Inlet Temp (°C)`
`2016-10-04T15:09:20,n1,19.0`
`2016-10-04T15:09:20,n2,18.0`
`2016-10-04T15:09:20,n3,18.0`
`2016-10-04T15:09:20,n4,17.0`
`2016-10-04T15:09:21,n1,19.0`
`2016-10-04T15:09:21,n2,18.0`
`2016-10-04T15:09:21,n3,18.0`
`2016-10-04T15:09:21,n4,17.0`
`2016-10-04T15:09:22,n1,19.0`
`2016-10-04T15:09:22,n2,18.0`
`2016-10-04T15:09:22,n3,18.0`
`2016-10-04T15:09:22,n4,17.0`
+69
View File
@@ -0,0 +1,69 @@
nodesetboot(8) -- Check or set next boot device for noderange
====================================================
## SYNOPSIS
`nodesetboot <noderange>`
`nodesetboot [options] <noderange> [default|cd|network|setup|hd]`
## DESCRIPTION
Requests that the next boot occur from the specified device. Unless otherwise
specified, this is a one time boot option, and does not change the normal boot
behavior of the system. This is useful for taking a system that normally boots
to the hard drive and startking a network install, or to go into the firmware
setup menu without having to hit a keystroke at the correct time on the console.
Generally, it's a bit more convenient and direct to use the nodeboot(8) command,
which will follow up the boot device with an immediate power directive to take
effect. The `nodesetboot` command is still useful, particularly if you want
to use `nodesetboot <noderange> setup` and then initiate a reboot from within
the operating system with ssh or similar rather than using the remote hardware
control.
## 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).
* `-p`, `--persist`:
For a system that supports it, mark the boot override to persist rather than
be a one time change. Many systems do not support this functionality.
* `default`:
Request a normal default boot with no particular device override
* `cd`:
Request boot from media. Note that this can include physical CD,
remote media mounted as CD/DVD, and detachable hard disks drives such as usb
key devices.
* `network`:
Request boot to network
* `setup`:
Request to enter the firmware configuration menu (e.g. F1 setup) on next boot.
* `hd`:
Boot straight to hard disk drive
## EXAMPLES
* Set next boot to setup for four nodes:
`# nodesetboot n1-n4 setup`
`n1: setup`
`n3: setup`
`n2: setup`
`n4: setup`
* Check boot override settings on four nodes:
`# nodesetboot n1-n4`
`n1: setup`
`n2: setup`
`n3: setup`
`n4: setup`
## SEE ALSO
nodeboot(8)
+34
View File
@@ -0,0 +1,34 @@
nodeshell(8) -- Execute command on many nodes in a noderange through ssh
=========================================================================
## SYNOPSIS
`nodeshell <noderange> [options] <command to execute on each node>`
## DESCRIPTION
Allows execution of a command on many nodes in parallel. Like noderun(8), it
accepts and interpolates confluent attribute expressions as documented in
nodeattribexpressions(5). `nodeshell` provides stdout as stdout and stderr
as stderr, unlike psh which combines all stdout and stderr into stdout.
## OPTIONS
* `-c`:
Specify the maximum number of instances to run concurrently.
## EXAMPLES
* Running `echo hi` on for nodes:
`# nodeshell n1-n4 echo hi`
`n1: hi`
`n2: hi`
`n3: hi`
`n4: hi`
* Setting a new static ip address temporarily on secondary interface of four nodes:
`# nodeshell n1-n4 ifconfig eth1 172.30.93.{n1}`
## SEE ALSO
noderun(8)
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
cd `dirname $0`/doc/man
mkdir -p ../../man/man1
mkdir -p ../../man/man5
mkdir -p ../../man/man8
ronn -r *.ronn
mv *.1 ../../man/man1/
mv *.5 ../../man/man5/
mv *.8 ../../man/man8/
+12 -2
View File
@@ -1,4 +1,14 @@
from setuptools import setup
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:
pass
setup(
name='confluent_client',
@@ -7,6 +17,6 @@ setup(
author_email='jjohnson2@lenovo.com',
url='http://xcat.sf.net/',
packages=['confluent'],
scripts=['bin/confetty', 'bin/nodeconsole', 'bin/nodeeventlog', 'bin/nodehealth', 'bin/nodeidentify', 'bin/nodelist', 'bin/nodepower', 'bin/nodesensors', 'bin/nodesetboot'],
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
scripts=scriptlist,
data_files=data_files,
)
+1
View File
@@ -1,2 +1,3 @@
include sysvinit/*
include systemd/*
include sysctl/*
+67
View File
@@ -0,0 +1,67 @@
#!/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 sys
import os
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.config.configmanager as cfm
import confluent.config.conf as conf
import confluent.main as main
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('-r', '--redact', action='store_true',
help='Redact potentially sensitive data rather than store')
argparser.add_option('-u', '--unprotected', action='store_true',
help='Specify that no password should be used to protect'
' the key information. Fields will be encrypted, '
'but keys.json will contain unencrypted decryption'
' keys that may be used to read the dump')
(options, args) = argparser.parse_args()
if len(args) != 2 or args[0] not in ('dump', 'restore'):
argparser.print_help()
sys.exit(1)
dumpdir = args[1]
if args[0] == 'restore':
pid = main.is_running()
if pid is not None:
print("Confluent is running, must shut down to restore db")
sys.exit(1)
cfm.restore_db_from_directory(dumpdir, options.password)
elif args[0] == 'dump':
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 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)
+3
View File
@@ -1,5 +1,8 @@
#!/bin/sh
cd `dirname $0`
if [ -x ./makeman ]; then
./makeman
fi
./makesetup
VERSION=`cat VERSION`
PKGNAME=$(basename $(pwd))
+1
View File
@@ -0,0 +1 @@
__version__ = "1.5.0.dev395.ggacb3a20"
+187
View File
@@ -0,0 +1,187 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Overall, the result of this shall be:
# - Web clients can create the same out-of-order responsiveness as socket
# clients (but with more complexity on their end)
# - Web clients can share single request among console sessions
# - Web clients can get async notify of things like node add/remove, events
# This provides an async strategy to http clients. The design is that a http
# session may have an 'async' resource. In such a case, any requests are
# queued and immediately the response is given accepting the queued request.
# A request flags itself as queue-compatible through an HTTP header indicating
# the identifier of the async thread. As responses happen to the queued
# request, data is dispatched to the first registered poller for data on
# the session. This way, a client may elect to provide multiple pollers
# to mitigate general choppiness of http network pattern. It may not be
# worth it, but it's possible.
# Additionally support console session multiplexing, to mitigate needed
# connection count.
# Also, this should allow a client to register for notifications of things
# like node add/delete or an event firing, ultimately.
# Much like console sessions, these will be reaped if a client spends too
# far away.
import collections
import confluent.exceptions as exc
import confluent.messages as messages
import confluent.util as util
import eventlet
import greenlet
import time
_asyncsessions = {}
_cleanthread = None
_consolesessions = None
def _assign_asyncid(asyncsession):
sessid = util.randomstring(32)
while sessid in _asyncsessions:
sessid = util.randomstring(32)
_asyncsessions[sessid] = {'asyncsession': asyncsession}
return sessid
class AsyncTermRelation(object):
# Need to keep an association of term object to async
# This allows the async handler to know the context of
# outgoing data to provide to calling code
def __init__(self, termid, async):
self.async = async
self.termid = termid
def got_data(self, data):
self.async.add(self.termid, data)
class AsyncSession(object):
def __init__(self):
self.asyncid = _assign_asyncid(self)
self.responses = collections.deque()
self._evt = None
self.termrelations = []
self.consoles = set([])
self.reaper = eventlet.spawn_after(15, self.destroy)
def add(self, requestid, rsp):
self.responses.append((requestid, rsp))
if self._evt:
self._evt.send()
self._evt = None
def set_term_relation(self, env):
# need a term relation to keep track of what data belongs
# to what object (since the callback does not provide context
# for data, and here ultimately the client is responsible
# for sorting out which is which.
termrel = AsyncTermRelation(env['HTTP_CONFLUENTREQUESTID'], self)
self.termrelations.append(termrel)
return termrel
def add_console_session(self, sessionid):
self.consoles.add(sessionid)
def destroy(self):
if self._evt:
self._evt.send()
self._evt = None
for console in self.consoles:
_consolesessions[console]['session'].destroy()
self.consoles = None
del _asyncsessions[self.asyncid]
def run_handler(self, handler, requestid):
try:
for rsp in handler:
self.add(requestid, rsp)
self.add(requestid, messages.AsyncCompletion())
except Exception as e:
self.add(requestid, e)
def get_responses(self, timeout=25):
self.reaper.cancel()
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
nextexpiry = time.time() + 90
for csess in list(self.consoles):
try:
_consolesessions[csess]['expiry'] = nextexpiry
except KeyError: # session has been closed elsewhere
self.consoles.discard(csess)
if self._evt:
# TODO(jjohnson2): This precludes the goal of 'double barreled'
# access.... revisit if this could matter
raise Exception('get_responses is not re-entrant')
if not self.responses: # wait to accumulate some
self._evt = eventlet.event.Event()
with eventlet.Timeout(timeout, False):
self._evt.wait()
self._evt = None
while self.responses:
yield self.responses.popleft()
def run_handler(hdlr, env):
asyncsessid = env['HTTP_CONFLUENTASYNCID']
try:
asyncsession = _asyncsessions[asyncsessid]['asyncsession']
requestid = env['HTTP_CONFLUENTREQUESTID']
except KeyError:
raise exc.InvalidArgumentException(
'Invalid Session ID or missing request id')
eventlet.spawn_n(asyncsession.run_handler, hdlr, requestid)
return requestid
def get_async(env, querydict):
global _cleanthread
return _asyncsessions[env['HTTP_CONFLUENTASYNCID']]['asyncsession']
def handle_async(env, querydict, threadset):
global _cleanthread
# This may be one of two things, a request for a new async stream
# or a request for next data from async stream
# httpapi otherwise handles requests an injecting them to queue
if 'asyncid' not in querydict or not querydict['asyncid']:
# This is a new request, create a new multiplexer
currsess = AsyncSession()
yield messages.AsyncSession(currsess.asyncid)
return
mythreadid = greenlet.getcurrent()
threadset.add(mythreadid)
loggedout = None
currsess = None
try:
currsess = _asyncsessions[querydict['asyncid']]['asyncsession']
for rsp in currsess.get_responses():
yield messages.AsyncMessage(rsp)
except greenlet.GreenletExit as ge:
loggedout = ge
threadset.discard(mythreadid)
if loggedout is not None:
currsess.destroy()
raise exc.LoggedOut()
def set_console_sessions(consolesessions):
global _consolesessions
_consolesessions = consolesessions
+18 -13
View File
@@ -37,6 +37,7 @@ _passcache = {}
_passchecking = {}
authworkers = None
authcleaner = None
class Credentials(object):
@@ -116,7 +117,7 @@ def authorize(name, element, tenant=False, operation='create',
user, tenant = _get_usertenant(name, tenant)
if tenant is not None and not configmanager.is_tenant(tenant):
return None
manager = configmanager.ConfigManager(tenant)
manager = configmanager.ConfigManager(tenant, username=user)
if skipuserobj:
return None, manager, user, tenant, skipuserobj
userobj = manager.get_user(user)
@@ -178,7 +179,7 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
# invalidate cache and force the slower check
del _passcache[(user, tenant)]
return None
cfm = configmanager.ConfigManager(tenant)
cfm = configmanager.ConfigManager(tenant, username=user)
ucfg = cfm.get_user(user)
if ucfg is None or 'cryptpass' not in ucfg:
eventlet.sleep(0.05) # stall even on test for existence of a username
@@ -195,6 +196,13 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
#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
@@ -211,19 +219,16 @@ def _apply_pbkdf(passphrase, salt):
lambda p, s: hmac.new(p, s, hashlib.sha256).digest())
def _clean_authworkers():
global authworkers
global authcleaner
authworkers = None
authcleaner = None
def _do_pbkdf(passphrase, salt):
# we must get it over to the authworkers pool or else get blocked in
# compute. However, we do want to wait for result, so we have
# one of the exceedingly rare sort of circumstances where 'apply'
# actually makes sense
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
def init_auth():
# have a some auth workers available. Keep them distinct from
# the general populace of workers to avoid unauthorized users
# starving out productive work
global authworkers
# for now we'll just have one auth worker and see if there is any
# demand for more. I personally doubt it.
authworkers = multiprocessing.Pool(processes=1)
return authworkers.apply(_apply_pbkdf, [passphrase, salt])
+243 -90
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,50 +16,69 @@
# limitations under the License.
#This defines the attributes of variou classes of things
#This defines the attributes of various classes of things
# 'nic', meant to be a nested structure under node
nic = {
'name': {
'description': 'Name in ip/ifconfig as desired by administrator',
},
'port': {
'description': 'Port that this nic connects to',
},
'switch': {
'description': 'Switch that this nic connects to',
},
'customhardwareaddress': {
'description': 'Mac address to push to nic',
},
'dnssuffix': {
'description': ('String to place after nodename, but before'
'Network.Domain to derive FQDN for this NIC'),
},
'hardwareaddress': {
'description': 'Active mac address on this nic (factory or custom)'
},
'ipaddresses': {
'description': 'Set of IPv4 and IPv6 addresses in CIDR format'
},
'pvid': {
'description': 'PVID of port on switch this nic connects to',
},
'mtu': {
'description': 'Requested MTU to configure on this interface',
},
'vlans': {
'description': 'Tagged VLANs to apply to nic/switch',
},
'dhcpv4enabled': {
'description': ('Whether DHCP should be attempted to acquire IPv4'
'address on this interface'),
},
'dhcpv6enabled': {
'description': ('Whether DHCP should be attempted to acquire IPv6'
'address on this interface'),
},
}
# changing mind on design, flattening to a single attribute, a *touch* less
# flexible at the top end, but much easier on the low end
# now net.<name>.attribute scheme
# similarly, leaning toward comma delimited ip addresses, since 99.99% of the
# time each nic will have one ip address
# vlan specification will need to be thought about a tad, each ip could be on
# a distinct vlan, but could have a vlan without an ip for sake of putting
# to a bridge. Current thought is
# vlans attribute would be comma delimited referring to the same index
# as addresses, with either 'native' or a number for vlan id
# the 'joinbridge' attribute would have some syntax like @<vlanid> to indicate
# joining only a vlan of the nic to the bridge
# 'joinbond' attribute would not support vlans.
#nic = {
# 'name': {
# 'description': 'Name in ip/ifconfig as desired by administrator',
# },
# 'biosdevname': {
# 'description': '"biosdevname" scheme to identify the adapter. If not'
# 'mac address match is preferred, then biosdevname, then'
# 'name.',
# },
# 'port': {
# 'description': 'Port that this nic connects to',
# },
# 'switch': {
# 'description': 'Switch that this nic connects to',
# },
# 'customhardwareaddress': {
# 'description': 'Mac address to push to nic',
# },
# 'dnssuffix': {
# 'description': ('String to place after nodename, but before'
# 'Network.Domain to derive FQDN for this NIC'),
# },
# 'hardwareaddress': {
# 'description': 'Active mac address on this nic (factory or custom)'
# },
# 'ipaddresses': {
# 'description': 'Set of IPv4 and IPv6 addresses in CIDR format'
# },
# 'pvid': {
# 'description': 'PVID of port on switch this nic connects to',
# },
# 'mtu': {
# 'description': 'Requested MTU to configure on this interface',
# },
# 'vlans': {
# 'description': 'Tagged VLANs to apply to nic/switch',
# },
# 'dhcpv4enabled': {
# 'description': ('Whether DHCP should be attempted to acquire IPv4'
# 'address on this interface'),
# },
# 'dhcpv6enabled': {
# 'description': ('Whether DHCP should be attempted to acquire IPv6'
# 'address on this interface'),
# },
#}
user = {
'password': {
@@ -70,7 +90,6 @@ user = {
node = {
'groups': {
'type': list,
'default': 'all',
'description': ('List of static groups for which this node is '
'considered a member'),
},
@@ -80,6 +99,84 @@ node = {
#'id': {
# 'description': ('Numeric identifier for node')
#},
# autonode is the feature of generating nodes based on connectivity to
# current node. In recursive autonode, for now we just allow endpoint to
# either be a server directly *or* a server enclosure. This precludes
# for the moment a concept of nested arbitrarily deep, but for now do this.
# hypothetically, one could imagine supporting an array and 'popping'
# names until reaching end. Not worth implementing at this point. If
# a traditional switch is added, it needs some care and feeding anyway.
# If a more exciting scheme presents itself, well we won't have to
# # own discovering switches anyway.
# 'autonode.servername': {
# 'description': ('Template for creating nodenames for automatic '
# 'creation of nodes detected as children of '
# 'this node. For example, a node in a server '
# 'enclosure bay or a server connected to a switch or '
# 'an enclosure manager connected to a switch. Certain '
# 'special template parameters are available and can '
# 'be used alongside usual config template directives. '
# '"discovered.nodenumber" will be replaced with the '
# 'bay or port number where the child node is connected.'
# ),
# },
# 'autonode.servergroups': {
# 'type': list,
# 'description': ('A list of groups to which discovered nodes will '
# 'belong to. As in autonode.servername, "discovered." '
# 'variable names will be substituted in special context')
# },
# 'autonode.enclosurename': {
# 'description': ('Template for creating nodenames when the discovered '
# 'node is an enclosure that will in turn generate nodes.'
# )
# },
# 'autonode.enclosuregroups': {
# 'type': list,
# 'description': ('A list of groups to which a discovered node will be'
# 'placed, presuming that node is an enclosure.')
# },
#For now, we consider this eventuality if needed. For now emphasize paradigm
# of group membership and see how far that goes.
# 'autonode.copyattribs': {
# 'type': list,
# 'description': ('A list of attributes to copy from the node generator '
# 'to the generated node. Expressions will be copied '
# 'over without evaluation, so will be evaluated '
# 'in the context of the generated node, rather than the'
# 'parent node. By default, an enclosure will copy over'
# 'autonode.servername, so that would not need to be '
# 'copied ')
# },
'discovery.policy': {
'description': 'Policy to use for auto-configuration of discovered '
'and identified nodes. Valid values are "manual", '
'"permissive", or "open". "manual" means nodes are '
'detected, but not autoconfigured until a user '
'approves. "permissive" indicates to allow discovery, '
'so long as the node has no existing public key. '
'"open" allows discovery even if a known public key '
'is already stored',
},
'info.note': {
'description': 'A field used for administrators to make arbitrary '
'notations about nodes. This is meant entirely for '
'human use and not programmatic use, so it can be '
'freeform text data without concern for issues in how '
'the server will process it.',
},
'location.room': {
'description': 'Room description for the node',
},
'location.row': {
'description': 'Row description for the rack the node is in',
},
'location.rack': {
'description': 'Rack number of the rack the node is in',
},
'location.u': {
'description': 'Position in the rack of the node',
},
# 'location.timezone': {
# 'description': 'POSIX timezone to apply to this node',
# },
@@ -175,54 +272,109 @@ node = {
# 'appliesto': ['vm'],
# },
'hardwaremanagement.manager': {
'description': 'The management address dedicated to this node',
'description': 'The management address dedicated to this node. This '
'is the address of, for example, the Lenovo IMM.',
},
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
},
# 'enclosure.manager': {
# 'description': "The management device for this node's chassis",
'enclosure.bay': {
'description': 'The bay in the enclosure, if any',
# 'appliesto': ['system'],
# },
# 'enclosure.bay': {
# 'description': 'The bay in the enclosure, if any',
},
'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.type': {
# 'description': '''The type of enclosure in use (e.g. IBM BladeCenter,
#IBM Flex)''',
# 'appliesto': ['system'],
# },
# 'inventory.serialnumber': {
# 'description': 'The manufacturer serial number of node',
# },
# 'inventory.uuid': {
# 'description': 'The UUID of the node as presented in DMI',
# },
# 'inventory.modelnumber': {
'id.model': {
'description': 'The model number of a node. In scenarios where there '
'is both a name and a model number, it is generally '
'expected that this would be the generally more '
'specific model number.'
},
'id.serial': {
'description': 'The manufacturer serial number of node',
},
'id.uuid': {
'description': 'The UUID of the node as presented in DMI.',
},
# For single interface mac collection, net.bootable suffices
# For multiple interface mac collection, we perhaps add an address field and use subnet affinity as a clue
# to disambiguate multiple addresses for external provisioning
# For internal provisioning, the UUID matters rather than the MAC, though subnet affinity may
# still be a factor to select the appropriate static config to send down. In such a case bonding
# is hopefully more likely as that's a bit easier.
# Start with the first case and only document that, the other thoughts can be future items if they turn up
'net.bootable': {
'type': bool,
'description': 'Whether or not the indicated network interface is to be used for booting. This is used by '
'the discovery process to decide where to place the mac address of a detected PXE nic.',
},
'net.ipv4_gateway': {
'description': 'The IPv4 gateway to use if applicable. As is the '
'case for other net attributes, net.eth0.ipv4_gateway '
'and similar is accepted.'
},
'net.hwaddr': {
'description': 'The hardware address, aka MAC address of the interface indicated, generally populated by the '
'PXE discovery mechanism'
},
# 'net.pxe': { 'description': 'Whether pxe will be used on this interface'
# TODO(jjohnson2): Above being 'true' will control whether mac addresses
# are stored in this nics attribute on pxe-client discovery, since
# pxe discovery is ambiguous for BMC and system on same subnet,
# or even both on the same port and same subnet
'net.switch': {
'description': 'An ethernet switch the node is connected to. Note '
'that net.* attributes may be indexed by interface. '
'For example instead of using net.switch, it is '
'possible to use net.eth0.switch and net.eth1.switch '
'or net.0.switch and net.1.switch to define multiple '
'sets of net connectivity associated with each other.'
},
'net.switchport': {
'description': 'The port on the switch that corresponds to this node. '
'See information on net.switch for more on the '
'flexibility of net.* attributes.'
},
# 'id.modelnumber': {
# 'description': 'The manufacturer dictated model number for the node',
# },
# 'inventory.snmpengineid': {
# 'id.modelname': {
# 'description': 'The manufacturer model label for the node',
# },
# 'id.snmpengineid': {
# 'description': 'The SNMP Engine id used by this node',
# },
# 'secret.snmpuser': {
# 'description': 'The user to use for SNMPv3 access to this node',
# },
# 'secret.snmppassphrase': {
# 'description': 'The passphrase to use for SNMPv3 access to this node',
# 'secret.snmppassword': {
# 'description': 'The password to use for SNMPv3 access to this node',
# },
'secret.snmpcommunity': {
'description': ('SNMPv1 community string, it is highly recommended to'
'step up to SNMPv3'),
},
# 'secret.snmplocalizedkey': {
# 'description': ("SNMPv3 key localized to this node's SNMP Engine id"
# 'This can be used in lieu of snmppassphrase to avoid'
# 'retaining the passphrase TODO: document procedure'
# 'to commit passphrase to localized key'),
# },
# 'secret.snmpcommunity': {
# 'description': ('SNMPv1 community string, it is highly recommended to'
# 'step up to SNMPv3'),
# },
# 'secret.localadminpassphrase': {
# 'secret.adminpassword': {
# 'description': ('The passphrase to apply to local root/administrator '
# 'account. '
# 'If the environment is 100% Linux, the value may be '
@@ -233,34 +385,35 @@ node = {
# 'AD')
# },
'secret.ipmikg': {
'description': 'Optional Integrity key for IPMI communication'
'description': 'Optional Integrity key for IPMI communication. This '
'should generally be ignored, as mutual authentication '
'is normally done with the password alone (which is a '
'shared secret in IPMI)'
},
# 'secret.ipmiuser': {
# 'description': ('The username to use to log into IPMI device related '
# 'to the node. For setting username, default '
# 'behavior is to randomize username, for using '
# 'username if not set, USERID is assumed'),
# },
# 'secret.ipmipassphrase': {
# 'description': ('The key to use to authenticate to IPMI device '
# 'related to the node. For setting passphrase, '
# 'default behavior is to randomize passphrase and '
# 'store it here. If going to connect over the '
# 'network and value is not set, PASSW0RD is attempted')
# },
'secret.hardwaremanagementuser': {
'description': ('Username to be set and used by protocols like SSH '
'and HTTP where client provides passphrase over the '
'network. Given the distinct security models betwen '
'this class of protocols and SNMP and IPMI, snmp and '
'ipmi utilize dedicated values.'),
'description': ('The username to use when connecting to the hardware '
'manager'),
},
'secret.hardwaremanagementpassword': {
'description': ('Passphrase to be set and used by protocols like SSH '
'and HTTP, where client sends passphrase over the '
'network. Given distinct security models between '
'this class of protocols, SNMP, and IPMI, SNMP and '
'IPMI are given their own settings with distinct '
'behaviors'),
'description': ('Password to use when connecting to the hardware '
'manager'),
},
'pubkeys.addpolicy': {
'description': ('Policy to use when encountering unknown public '
'keys. Choices are "automatic" to accept and '
'store new key if no key known and "manual" '
'to always reject a new key, even if no key known'
'Note that if the trusted CA verifies the certificate,'
' that is accepted ignoring this policy. Default '
'policy is "automatic"'),
'valid_values': ('automatic', 'manual'),
},
'pubkeys.tls_hardwaremanager': {
'description': ('Fingerprint of the TLS certificate recognized as'
'belonging to the hardware manager of the server'),
},
'pubkeys.ssh': {
'description': ('Fingerprint of the SSH key of the OS running on the '
'system.'),
},
}
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -65,12 +65,16 @@ import anydbm as dbm
import ast
import base64
import confluent.config.attributes as allattributes
import confluent.config.conf as conf
import confluent.log
import confluent.noderange as noderange
import confluent.util
import confluent.exceptions as exc
import copy
import cPickle
import errno
import eventlet
import fnmatch
import json
import operator
import os
@@ -87,6 +91,14 @@ _masterintegritykey = None
_dirtylock = threading.RLock()
_config_areas = ('nodegroups', 'nodes', 'usergroups', 'users')
tracelog = None
statelessmode = False
_cfgstore = None
_attraliases = {
'bmc': 'hardwaremanagement.manager',
'bmcuser': 'secret.hardwaremanagementuser',
'bmcpass': 'secret.hardwaremanagementpassword',
}
def _mkpath(pathname):
try:
@@ -128,6 +140,18 @@ def _get_protected_key(keydict, password, paramname):
raise exc.LockedCredentials("No available decryption key")
def _parse_key(keydata, password=None):
if keydata.startswith('*unencrypted:'):
return base64.b64decode(keydata[13:])
elif password:
salt, iv, crypt, hmac = [base64.b64decode(x)
for x in keydata.split('!')]
privkey, integkey = _derive_keys(password, salt)
return decrypt_value([iv, crypt, hmac], privkey, integkey)
raise(exc.LockedCredentials(
"Passphrase protected secret requires password"))
def _format_key(key, password=None):
if password is not None:
salt = os.urandom(32)
@@ -138,6 +162,29 @@ def _format_key(key, password=None):
return {"unencryptedvalue": key}
def _do_notifier(cfg, watcher, callback):
try:
callback(nodeattribs=watcher['nodeattrs'], configmanager=cfg)
except Exception:
logException()
def logException():
global tracelog
if tracelog is None:
tracelog = confluent.log.Logger('trace')
tracelog.log(traceback.format_exc(),
ltype=confluent.log.DataTypes.event,
event=confluent.log.Events.stacktrace)
def _do_add_watcher(watcher, added, configmanager):
try:
watcher(added=added, deleting=[], configmanager=configmanager)
except Exception:
logException()
def init_masterkey(password=None):
global _masterkey
global _masterintegritykey
@@ -185,6 +232,49 @@ def decrypt_value(cryptvalue,
return value[0:-padsize]
def fixup_attribute(attrname, attrval):
# Normalize some data, for example strings and numbers to bool
attrname = _get_valid_attrname(attrname)
if attrname not in allattributes.node: # no fixup possible
return attrval
if 'type' in allattributes.node[attrname] and not isinstance(attrval, allattributes.node[attrname]['type']):
if (allattributes.node[attrname]['type'] == bool and
(isinstance(attrval, str) or isinstance(attrval, unicode))):
return attrval.lower() in ('true', '1', 'y', 'yes', 'enable', 'enabled')
return attrval
def attribute_is_invalid(attrname, attrval):
if attrname.startswith('custom.'):
# No type checking or name checking is provided for custom,
# it's not possible
return False
attrname = _get_valid_attrname(attrname)
if attrname not in allattributes.node:
# Otherwise, it must be in the allattributes key list
return True
if 'type' in allattributes.node[attrname]:
if not isinstance(attrval, allattributes.node[attrname]['type']):
# it is valid if it is {'value': actualvalue}
if (isinstance(attrval, dict) and 'value' in attrval and
isinstance(attrval['value'],
allattributes.node[attrname]['type'])):
return False
# provide type checking for attributes with a specific type
return True
return False
def _get_valid_attrname(attrname):
if attrname.startswith('net.'):
# For net.* attribtues, split on the dots and put back together
# longer term we might want a generic approach, but
# right now it's just net. attributes
netattrparts = attrname.split('.')
attrname = netattrparts[0] + '.' + netattrparts[-1]
return attrname
def crypt_value(value,
key=None,
integritykey=None):
@@ -201,7 +291,10 @@ def crypt_value(value,
neededpad = 16 - (len(value) % 16)
pad = chr(neededpad) * neededpad
value += pad
cryptval = crypter.encrypt(value)
try:
cryptval = crypter.encrypt(value)
except TypeError:
cryptval = crypter.encrypt(value.encode('utf-8'))
hmac = HMAC.new(integritykey, cryptval, SHA256).digest()
return iv, cryptval, hmac
@@ -214,8 +307,14 @@ def _load_dict_from_dbm(dpath, tdb):
if elem not in currdict:
currdict[elem] = {}
currdict = currdict[elem]
for tk in dbe.iterkeys():
currdict[tk] = cPickle.loads(dbe[tk])
try:
for tk in dbe:
currdict[tk] = cPickle.loads(dbe[tk])
except AttributeError:
tk = dbe.firstkey()
while tk != None:
currdict[tk] = cPickle.loads(dbe[tk])
tk = dbe.nextkey(tk)
except dbm.error:
return
@@ -233,6 +332,8 @@ def get_global(globalname):
:param globalname: The global parameter name to read
"""
if _cfgstore is None:
init()
try:
return _cfgstore['globals'][globalname]
except KeyError:
@@ -249,6 +350,8 @@ def set_global(globalname, value):
:param globalname: The global parameter name to store
:param value: The value to set the global parameter to.
"""
if _cfgstore is None:
init()
with _dirtylock:
if 'dirtyglobals' not in _cfgstore:
_cfgstore['dirtyglobals'] = set()
@@ -319,20 +422,27 @@ class _ExpressionFormat(string.Formatter):
return node.n
elif isinstance(node, ast.Attribute):
#ok, we have something with a dot
left = node.value.id
right = node.attr
key = left + '.' + right
if '_expressionkeys' not in self._nodeobj:
self._nodeobj['_expressionkeys'] = set([key])
else:
self._nodeobj['_expressionkeys'].add(key)
val = _decode_attribute(key, self._nodeobj,
formatter=self)
return val['value'] if 'value' in val else ""
left = node
key = ''
while isinstance(left, ast.Attribute):
# Loop through, to handle multi dot expressions
# such as 'net.pxe.hwaddr'
key = '.' + left.attr + key
left = left.value
key = left.id + key
if (not key.startswith('custom.') and
_get_valid_attrname(key) not in allattributes.node):
raise ValueError(
'{0} is not a valid attribute name'.format(key))
val = self._expand_attribute(key)
return val['value'] if val and 'value' in val else ""
elif isinstance(node, ast.Name):
var = node.id
if var == 'nodename':
if var in ('node', 'nodename'):
return self._nodename
if var in _attraliases:
val = self._expand_attribute(_attraliases[var])
return val['value'] if 'value' in val else ""
mg = re.match(self.posmatch, var)
if mg:
idx = int(mg.group(1))
@@ -341,20 +451,28 @@ class _ExpressionFormat(string.Formatter):
return int(self._numbers[idx - 1])
else:
if var in self._nodeobj:
if '_expressionkeys' not in self._nodeobj:
self._nodeobj['_expressionkeys'] = set([var])
else:
self._nodeobj['_expressionkeys'].add(var)
val = _decode_attribute(var, self._nodeobj,
formatter=self)
return val['value'] if 'value' in val else ""
val = self._expand_attribute(var)
return val['value'] if val and 'value' in val else ""
elif (not var.startswith('custom.') and
_get_valid_attrname(var) not in allattributes.node):
raise ValueError(
'{0} is not a valid attribute name'.format(var))
elif isinstance(node, ast.BinOp):
optype = type(node.op)
if optype not in self._supported_ops:
raise Exception("Unsupported operation")
op = self._supported_ops[optype]
return op(self._handle_ast_node(node.left),
self._handle_ast_node(node.right))
return op(int(self._handle_ast_node(node.left)),
int(self._handle_ast_node(node.right)))
def _expand_attribute(self, key):
if '_expressionkeys' not in self._nodeobj:
self._nodeobj['_expressionkeys'] = set([key])
else:
self._nodeobj['_expressionkeys'].add(key)
val = _decode_attribute(key, self._nodeobj,
formatter=self)
return val
def _decode_attribute(attribute, nodeobj, formatter=None, decrypt=False):
@@ -409,7 +527,7 @@ def hook_new_configmanagers(callback):
#TODO(jbjohnso): actually live up to the promise of ongoing callbacks
callback(ConfigManager(None))
try:
for tenant in _cfgstore['tenant'].iterkeys():
for tenant in _cfgstore['tenant']:
callback(ConfigManager(tenant))
except KeyError:
pass
@@ -429,9 +547,12 @@ class ConfigManager(object):
_nodecollwatchers = {}
_notifierids = {}
def __init__(self, tenant, decrypt=False):
def __init__(self, tenant, decrypt=False, username=None):
global _cfgstore
if _cfgstore is None:
init()
self.decrypt = decrypt
self.current_user = username
if tenant is None:
self.tenant = None
if 'main' not in _cfgstore:
@@ -531,7 +652,9 @@ class ConfigManager(object):
def watch_attributes(self, nodes, attributes, callback):
"""
Watch a list of attributes for changes on a list of nodes
Watch a list of attributes for changes on a list of nodes. The
attributes may be literal, or a filename style wildcard like
'net*.switch'
:param nodes: An iterable of node names to be watching
:param attributes: An iterable of attribute names to be notified about
@@ -559,6 +682,10 @@ class ConfigManager(object):
}
else:
attribwatchers[node][attribute][notifierid] = callback
if '*' in attribute:
currglobs = attribwatchers[node].get('_attrglobs', set([]))
currglobs.add(attribute)
attribwatchers[node]['_attrglobs'] = currglobs
return notifierid
def watch_nodecollection(self, callback):
@@ -606,7 +733,7 @@ class ConfigManager(object):
def list_users(self):
try:
return self._cfgstore['users'].iterkeys()
return list(self._cfgstore['users'])
except KeyError:
return []
@@ -648,7 +775,7 @@ class ConfigManager(object):
:param attributemap: The mapping of keys to values to set
"""
for attribute in attributemap.iterkeys():
for attribute in attributemap:
self._cfgstore['usergroups'][attribute] = attributemap[attribute]
_mark_dirtykey('usergroups', groupname, self.tenant)
@@ -700,6 +827,8 @@ class ConfigManager(object):
:param uid: Custom identifier number if desired. Defaults to random.
:param displayname: Optional long format name for UI consumption
"""
if 'idmap' not in _cfgstore['main']:
_cfgstore['main']['idmap'] = {}
if uid is None:
uid = _generate_new_id()
else:
@@ -713,8 +842,6 @@ class ConfigManager(object):
self._cfgstore['users'][name] = {'id': uid}
if displayname is not None:
self._cfgstore['users'][name]['displayname'] = displayname
if 'idmap' not in _cfgstore['main']:
_cfgstore['main']['idmap'] = {}
_cfgstore['main']['idmap'][uid] = {
'tenant': self.tenant,
'username': name
@@ -731,19 +858,23 @@ class ConfigManager(object):
def is_nodegroup(self, nodegroup):
return nodegroup in self._cfgstore['nodegroups']
def get_groups(self):
return self._cfgstore['nodegroups'].iterkeys()
def get_groups(self, sizesort=False):
if sizesort:
return reversed(
sorted(self._cfgstore['nodegroups'], key=lambda x: len(
self._cfgstore['nodegroups'][x]['nodes'])))
return iter(self._cfgstore['nodegroups'])
def list_nodes(self):
try:
return self._cfgstore['nodes'].iterkeys()
return iter(self._cfgstore['nodes'])
except KeyError:
return []
def get_nodegroup_attributes(self, nodegroup, attributes=()):
cfgnodeobj = self._cfgstore['nodegroups'][nodegroup]
if not attributes:
attributes = cfgnodeobj.iterkeys()
attributes = cfgnodeobj
nodeobj = {}
for attribute in attributes:
if attribute.startswith('_'):
@@ -754,11 +885,23 @@ class ConfigManager(object):
decrypt=self.decrypt)
return nodeobj
def get_node_attributes(self, nodelist, attributes=()):
def expand_attrib_expression(self, nodelist, expression):
if type(nodelist) in (unicode, str):
nodelist = (nodelist,)
for node in nodelist:
cfgobj = self._cfgstore['nodes'][node]
fmt = _ExpressionFormat(cfgobj, node)
yield (node, fmt.format(expression))
def get_node_attributes(self, nodelist, attributes=(), decrypt=None):
if decrypt is None:
decrypt = self.decrypt
retdict = {}
relattribs = attributes
if isinstance(nodelist, str) or isinstance(nodelist, unicode):
nodelist = [nodelist]
if isinstance(attributes, str) or isinstance(attributes, unicode):
attributes = [attributes]
relattribs = attributes
for node in nodelist:
if node not in self._cfgstore['nodes']:
continue
@@ -770,13 +913,17 @@ class ConfigManager(object):
if attribute.startswith('_'):
# skip private things
continue
if '*' in attribute:
for attr in fnmatch.filter(list(cfgnodeobj), attribute):
nodeobj[attr] = _decode_attribute(attr, cfgnodeobj,
decrypt=decrypt)
if attribute not in cfgnodeobj:
continue
# since the formatter is not passed in, the calculator is
# skipped. The decryption, however, we want to do only on
# demand
nodeobj[attribute] = _decode_attribute(attribute, cfgnodeobj,
decrypt=self.decrypt)
decrypt=decrypt)
retdict[node] = nodeobj
return retdict
@@ -786,7 +933,7 @@ class ConfigManager(object):
groupcfg = self._cfgstore['nodegroups'][group]
except KeyError: # something did not exist, nothing to do
return
for attrib in groupcfg.iterkeys():
for attrib in groupcfg:
self._do_inheritance(nodecfg, attrib, node, changeset)
_addchange(changeset, node, attrib)
@@ -840,7 +987,7 @@ class ConfigManager(object):
return
def _sync_groups_to_node(self, groups, node, changeset):
for group in self._cfgstore['nodegroups'].iterkeys():
for group in self._cfgstore['nodegroups']:
if group not in groups:
if node in self._cfgstore['nodegroups'][group]['nodes']:
self._cfgstore['nodegroups'][group]['nodes'].discard(node)
@@ -857,7 +1004,7 @@ class ConfigManager(object):
self._node_added_to_group(node, group, changeset)
def _sync_nodes_to_group(self, nodes, group, changeset):
for node in self._cfgstore['nodes'].iterkeys():
for node in self._cfgstore['nodes']:
if node not in nodes and 'groups' in self._cfgstore['nodes'][node]:
if group in self._cfgstore['nodes'][node]['groups']:
self._cfgstore['nodes'][node]['groups'].remove(group)
@@ -878,38 +1025,70 @@ class ConfigManager(object):
def set_group_attributes(self, attribmap, autocreate=False):
changeset = {}
for group in attribmap.iterkeys():
for group in attribmap:
if group == '':
raise ValueError('"{0}" is not a valid group name'.format(
group))
if autocreate:
try:
noderange._parser.parseString(
'({0})'.format(group)).asList()
except noderange.pp.ParseException as pe:
raise ValueError('"{0}" is not a valid group name'.format(
group))
if not autocreate and group not in self._cfgstore['nodegroups']:
raise ValueError("{0} group does not exist".format(group))
for attr in attribmap[group].iterkeys():
if (attr not in ('nodes', 'noderange') and
(attr not in allattributes.node or
('type' in allattributes.node[attr] and
not isinstance(attribmap[group][attr],
allattributes.node[attr]['type'])))):
raise ValueError
for attr in attribmap[group]:
# first do a pass to normalize out any aliased attribute names
if attr in _attraliases:
newattr = _attraliases[attr]
attribmap[group][newattr] = attribmap[group][attr]
del attribmap[group][attr]
for attr in attribmap[group]:
if attr in _attraliases:
newattr = _attraliases[attr]
attribmap[group][newattr] = attribmap[group][attr]
del attribmap[group][attr]
if attr not in ('nodes', 'noderange'):
attrval = fixup_attribute(attr, attribmap[group][attr])
if attribute_is_invalid(attr, attrval):
errstr = "{0} attribute is invalid".format(attr)
raise ValueError(errstr)
attribmap[group][attr] = attrval
if attr == 'nodes':
if isinstance(attribmap[group][attr], dict):
currnodes = list(self.get_nodegroup_attributes(
group, ['nodes']).get('nodes', []))
if attribmap[group][attr].get('prepend', False):
newnodes = attribmap[group][attr][
'prepend'].split(',')
attribmap[group][attr] = newnodes + currnodes
elif attribmap[group][attr].get('remove', False):
delnodes = attribmap[group][attr][
'remove'].split(',')
attribmap[group][attr] = [
x for x in currnodes if x not in delnodes]
if not isinstance(attribmap[group][attr], list):
raise ValueError(
"nodes attribute on group must be list")
if type(attribmap[group][attr]) is unicode or type(attribmap[group][attr]) is str:
attribmap[group][attr]=attribmap[group][attr].split(",")
else:
raise ValueError("nodes attribute on group must be list")
for node in attribmap[group]['nodes']:
if node not in self._cfgstore['nodes']:
raise ValueError(
"{0} node does not exist to add to {1}".format(
node, group))
for group in attribmap.iterkeys():
for group in attribmap:
group = group.encode('utf-8')
if group not in self._cfgstore['nodegroups']:
self._cfgstore['nodegroups'][group] = {'nodes': set()}
cfgobj = self._cfgstore['nodegroups'][group]
for attr in attribmap[group].iterkeys():
for attr in attribmap[group]:
if attr == 'nodes':
newdict = set(attribmap[group][attr])
elif (isinstance(attribmap[group][attr], str) or
isinstance(attribmap[group][attr], unicode)):
isinstance(attribmap[group][attr], unicode) or
isinstance(attribmap[group][attr], bool)):
newdict = {'value': attribmap[group][attr]}
else:
newdict = attribmap[group][attr]
@@ -933,6 +1112,13 @@ class ConfigManager(object):
def clear_group_attributes(self, groups, attributes):
changeset = {}
realattributes = []
for attrname in list(attributes):
if attrname in _attraliases:
realattributes.append(_attraliases[attrname])
else:
realattributes.append(attrname)
attributes = realattributes
if type(groups) in (str, unicode):
groups = (groups,)
for group in groups:
@@ -987,7 +1173,7 @@ class ConfigManager(object):
return
notifdata = {}
attribwatchers = self._attribwatchers[self.tenant]
for node in nodeattrs.iterkeys():
for node in nodeattrs:
if node not in attribwatchers:
continue
attribwatcher = attribwatchers[node]
@@ -1000,10 +1186,21 @@ class ConfigManager(object):
# to deletion, to make all watchers aware of the removed
# node and take appropriate action
checkattrs = attribwatcher
globattrs = {}
for attrglob in attribwatcher.get('_attrglobs', []):
for matched in fnmatch.filter(list(checkattrs), attrglob):
globattrs[matched] = attrglob
for attrname in checkattrs:
if attrname not in attribwatcher:
if attrname == '_attrglobs':
continue
for notifierid in attribwatcher[attrname].iterkeys():
watchkey = attrname
# the attrib watcher could still have a glob
if attrname not in attribwatcher:
if attrname in globattrs:
watchkey = globattrs[attrname]
else:
continue
for notifierid in attribwatcher[watchkey]:
if notifierid in notifdata:
if node in notifdata[notifierid]['nodeattrs']:
notifdata[notifierid]['nodeattrs'][node].append(
@@ -1014,18 +1211,12 @@ class ConfigManager(object):
else:
notifdata[notifierid] = {
'nodeattrs': {node: [attrname]},
'callback': attribwatcher[attrname][notifierid]
'callback': attribwatcher[watchkey][notifierid]
}
for watcher in notifdata.itervalues():
callback = watcher['callback']
try:
callback(nodeattribs=watcher['nodeattrs'], configmanager=self)
except Exception:
global tracelog
if tracelog is None:
tracelog = confluent.log.Logger('trace')
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
eventlet.spawn_n(_do_notifier, self, watcher, callback)
def del_nodes(self, nodes):
if self.tenant in self._nodecollwatchers:
@@ -1059,6 +1250,13 @@ class ConfigManager(object):
def clear_node_attributes(self, nodes, attributes):
# accumulate all changes into a changeset and push in one go
changeset = {}
realattributes = []
for attrname in list(attributes):
if attrname in _attraliases:
realattributes.append(_attraliases[attrname])
else:
realattributes.append(attrname)
attributes = realattributes
for node in nodes:
node = node.encode('utf-8')
try:
@@ -1085,7 +1283,7 @@ class ConfigManager(object):
self._bg_sync_to_file()
def add_node_attributes(self, attribmap):
for node in attribmap.iterkeys():
for node in attribmap:
if 'groups' not in attribmap[node]:
attribmap[node]['groups'] = []
self.set_node_attributes(attribmap, autocreate=True)
@@ -1098,23 +1296,48 @@ class ConfigManager(object):
changeset = {}
# first do a sanity check of the input upfront
# this mitigates risk of arguments being partially applied
for node in attribmap.iterkeys():
for node in attribmap:
node = node.encode('utf-8')
if node == '':
raise ValueError('"{0}" is not a valid node name'.format(node))
if autocreate:
try:
noderange._parser.parseString(
'({0})'.format(node)).asList()
except noderange.pp.ParseException as pe:
raise ValueError(
'"{0}" is not a valid node name'.format(node))
if autocreate is False and node not in self._cfgstore['nodes']:
raise ValueError("node {0} does not exist".format(node))
for attrname in attribmap[node].iterkeys():
for attrname in list(attribmap[node]):
if attrname in _attraliases:
truename = _attraliases[attrname]
attribmap[node][truename] = attribmap[node][attrname]
del attribmap[node][attrname]
for attrname in attribmap[node]:
attrval = attribmap[node][attrname]
if (attrname not in allattributes.node or
('type' in allattributes.node[attrname] and
not isinstance(
attrval,
allattributes.node[attrname]['type']))):
errstr = "{0} attribute on node {1} is invalid".format(
attrname, node)
raise ValueError(errstr)
try:
if (allattributes.node[attrname]['type'] == 'list' and
type(attrval) in (str, unicode)):
attrval = attrval.split(",")
except KeyError:
pass
if attrname == 'groups':
if isinstance(attribmap[node]['groups'], dict):
currgroups = self.get_node_attributes(
node, 'groups').get(node, {}).get('groups', [])
if attribmap[node]['groups'].get('prepend', False):
newgroups = attribmap[node]['groups'][
'prepend'].split(',')
attribmap[node]['groups'] = newgroups + currgroups
elif attribmap[node]['groups'].get('remove', False):
delgroups = attribmap[node]['groups'][
'remove'].split(',')
newgroups = [
x for x in currgroups if x not in delgroups]
attribmap[node]['groups'] = newgroups
elif type(attribmap[node]['groups']) != list:
attribmap[node]['groups']=attribmap[node]['groups'].split(",")
for group in attribmap[node]['groups']:
if group not in self._cfgstore['nodegroups']:
raise ValueError(
@@ -1122,7 +1345,14 @@ class ConfigManager(object):
if ('everything' in self._cfgstore['nodegroups'] and
'everything' not in attribmap[node]['groups']):
attribmap[node]['groups'].append('everything')
for node in attribmap.iterkeys():
else:
attrval = fixup_attribute(attrname, attrval)
if attribute_is_invalid(attrname, attrval):
errstr = "{0} attribute on node {1} is invalid".format(
attrname, node)
raise ValueError(errstr)
attribmap[node][attrname] = attrval
for node in attribmap:
node = node.encode('utf-8')
exprmgr = None
if node not in self._cfgstore['nodes']:
@@ -1130,9 +1360,10 @@ class ConfigManager(object):
self._cfgstore['nodes'][node] = {}
cfgobj = self._cfgstore['nodes'][node]
recalcexpressions = False
for attrname in attribmap[node].iterkeys():
for attrname in attribmap[node]:
if (isinstance(attribmap[node][attrname], str) or
isinstance(attribmap[node][attrname], unicode)):
isinstance(attribmap[node][attrname], unicode) or
isinstance(attribmap[node][attrname], bool)):
newdict = {'value': attribmap[node][attrname]}
else:
newdict = attribmap[node][attrname]
@@ -1166,10 +1397,76 @@ class ConfigManager(object):
if self.tenant in self._nodecollwatchers:
nodecollwatchers = self._nodecollwatchers[self.tenant]
for watcher in nodecollwatchers.itervalues():
watcher(added=newnodes, deleting=[], configmanager=self)
eventlet.spawn_n(_do_add_watcher, watcher, newnodes, self)
self._bg_sync_to_file()
#TODO: wait for synchronization to suceed/fail??)
def _load_from_json(self, jsondata):
"""Load fresh configuration data from jsondata
:param jsondata: String of jsondata
:return:
"""
dumpdata = json.loads(jsondata)
tmpconfig = {}
for confarea in _config_areas:
if confarea not in dumpdata:
continue
tmpconfig[confarea] = {}
for element in dumpdata[confarea]:
newelement = copy.deepcopy(dumpdata[confarea][element])
for attribute in dumpdata[confarea][element]:
if newelement[attribute] == '*REDACTED*':
raise Exception(
"Unable to restore from redacted backup")
elif attribute == 'cryptpass':
passparts = newelement[attribute].split('!')
newelement[attribute] = tuple([base64.b64decode(x)
for x in passparts])
elif 'cryptvalue' in newelement[attribute]:
bincrypt = newelement[attribute]['cryptvalue']
bincrypt = tuple([base64.b64decode(x)
for x in bincrypt.split('!')])
newelement[attribute]['cryptvalue'] = bincrypt
elif attribute in ('nodes', '_expressionkeys'):
# A group with nodes
# delete it and defer until nodes are being added
# which will implicitly fill this up
# Or _expressionkeys attribute, which will similarly
# be rebuilt
del newelement[attribute]
tmpconfig[confarea][element] = newelement
# We made it through above section without an exception, go ahead and
# replace
# Start by erasing the dbm files if present
for confarea in _config_areas:
try:
os.unlink(os.path.join(self._cfgdir, confarea))
except OSError as e:
if e.errno == 2:
pass
# Now we have to iterate through each fixed up element, using the
# set attribute to flesh out inheritence and expressions
_cfgstore['main']['idmap'] = {}
for confarea in _config_areas:
self._cfgstore[confarea] = {}
if confarea not in tmpconfig:
continue
if confarea == 'nodes':
self.set_node_attributes(tmpconfig[confarea], True)
elif confarea == 'nodegroups':
self.set_group_attributes(tmpconfig[confarea], True)
elif confarea == 'users':
for user in tmpconfig[confarea]:
uid = tmpconfig[confarea].get('id', None)
displayname = tmpconfig[confarea].get('displayname', None)
self.create_user(user, uid=uid, displayname=displayname)
if 'cryptpass' in tmpconfig[confarea][user]:
self._cfgstore['users'][user]['cryptpass'] = \
tmpconfig[confarea][user]['cryptpass']
_mark_dirtykey('users', user, self.tenant)
self._bg_sync_to_file()
def _dump_to_json(self, redact=None):
"""Dump the configuration in json form to output
@@ -1188,10 +1485,10 @@ class ConfigManager(object):
if confarea not in self._cfgstore:
continue
dumpdata[confarea] = {}
for element in self._cfgstore[confarea].iterkeys():
for element in self._cfgstore[confarea]:
dumpdata[confarea][element] = \
copy.deepcopy(self._cfgstore[confarea][element])
for attribute in self._cfgstore[confarea][element].iterkeys():
for attribute in self._cfgstore[confarea][element]:
if 'inheritedfrom' in dumpdata[confarea][element][attribute]:
del dumpdata[confarea][element][attribute]
elif (attribute == 'cryptpass' or
@@ -1237,14 +1534,20 @@ class ConfigManager(object):
pass
@classmethod
def shutdown(cls):
def wait_for_sync(cls):
cls._bg_sync_to_file()
if cls._cfgwriter is not None:
cls._cfgwriter.join()
@classmethod
def shutdown(cls):
cls.wait_for_sync()
sys.exit(0)
@classmethod
def _bg_sync_to_file(cls):
if statelessmode:
return
with cls._syncstate:
if cls._syncrunning:
cls._writepending = True
@@ -1258,6 +1561,8 @@ class ConfigManager(object):
@classmethod
def _sync_to_file(cls):
if statelessmode:
return
if 'dirtyglobals' in _cfgstore:
with _dirtylock:
dirtyglobals = copy.deepcopy(_cfgstore['dirtyglobals'])
@@ -1278,7 +1583,7 @@ class ConfigManager(object):
with _dirtylock:
currdirt = copy.deepcopy(_cfgstore['dirtykeys'])
del _cfgstore['dirtykeys']
for tenant in currdirt.iterkeys():
for tenant in currdirt:
dkdict = currdirt[tenant]
if tenant is None:
pathname = cls._cfgdir
@@ -1286,7 +1591,7 @@ class ConfigManager(object):
else:
pathname = os.path.join(cls._cfgdir, 'tenants', tenant)
currdict = _cfgstore['tenant'][tenant]
for category in dkdict.iterkeys():
for category in dkdict:
_mkpath(pathname)
dbf = dbm.open(os.path.join(pathname, category), 'c', 384) # 0600
try:
@@ -1309,7 +1614,7 @@ class ConfigManager(object):
return cls._sync_to_file()
def _recalculate_expressions(self, cfgobj, formatter, node, changeset):
for key in cfgobj.iterkeys():
for key in cfgobj:
if not isinstance(cfgobj[key], dict):
continue
if 'expression' in cfgobj[key]:
@@ -1323,39 +1628,95 @@ class ConfigManager(object):
self._recalculate_expressions(cfgobj[key], formatter, node,
changeset)
def _restore_keys(jsond, password, newpassword=None):
# the jsond from the restored file, password (if any) used to protect
# the file, and newpassword to use, (also check the service.cfg file)
global _masterkey
global _masterintegritykey
keydata = json.loads(jsond)
cryptkey = _parse_key(keydata['cryptkey'], password)
integritykey = _parse_key(keydata['integritykey'], password)
conf.init_config()
cfg = conf.get_config()
if cfg.has_option('security', 'externalcfgkey'):
keyfilename = cfg.get('security', 'externalcfgkey')
with open(keyfilename, 'r') as keyfile:
newpassword = keyfile.read()
set_global('master_privacy_key', _format_key(cryptkey,
password=newpassword))
set_global('master_integrity_key', _format_key(integritykey,
password=newpassword))
_masterkey = cryptkey
_masterintegritykey = integritykey
ConfigManager.wait_for_sync()
# At this point, we should have the key situation all sorted
def _dump_keys(password):
if _masterkey is None or _masterintegritykey is None:
init_masterkey()
cryptkey = _format_key(_masterkey, password=password)
cryptkey = '!'.join(map(base64.b64encode, cryptkey['passphraseprotected']))
if 'passphraseprotected' in cryptkey:
cryptkey = '!'.join(map(base64.b64encode,
cryptkey['passphraseprotected']))
else:
cryptkey = '*unencrypted:{0}'.format(base64.b64encode(
cryptkey['unencryptedvalue']))
integritykey = _format_key(_masterintegritykey, password=password)
integritykey = '!'.join(map(base64.b64encode, integritykey['passphraseprotected']))
if 'passphraseprotected' in integritykey:
integritykey = '!'.join(map(base64.b64encode,
integritykey['passphraseprotected']))
else:
integritykey = '*unencrypted:{0}'.format(base64.b64encode(
integritykey['unencryptedvalue']))
return json.dumps({'cryptkey': cryptkey, 'integritykey': integritykey},
sort_keys=True, indent=4, separators=(',', ': '))
def restore_db_from_directory(location, password):
try:
with open(os.path.join(location, 'keys.json'), 'r') as cfgfile:
keydata = cfgfile.read()
json.loads(keydata)
_restore_keys(keydata, password)
except IOError as e:
if e.errno == 2:
raise Exception("Cannot restore without keys, this may be a "
"redacted dump")
with open(os.path.join(location, 'main.json'), 'r') as cfgfile:
cfgdata = cfgfile.read()
ConfigManager(tenant=None)._load_from_json(cfgdata)
def dump_db_to_directory(location, password, redact=None):
with open(os.path.join(location, 'keys.json'), 'w') as cfgfile:
cfgfile.write(_dump_keys(password))
cfgfile.write('\n')
if not redact:
with open(os.path.join(location, 'keys.json'), 'w') as cfgfile:
cfgfile.write(_dump_keys(password))
cfgfile.write('\n')
with open(os.path.join(location, 'main.json'), 'w') as cfgfile:
cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact))
cfgfile.write('\n')
try:
for tenant in os.listdir(
os.path.join(ConfigManager._cfgdir, '/tenants/')):
with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile:
with open(os.path.join(location, 'tenants', tenant,
'main.json'), 'w') as cfgfile:
cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json(
redact=redact))
cfgfile.write('\n')
except OSError:
pass
try:
ConfigManager._read_from_path()
except IOError:
_cfgstore = {}
def init(stateless=False):
global _cfgstore
if stateless:
_cfgstore = {}
return
try:
ConfigManager._read_from_path()
except IOError:
_cfgstore = {}
# some unit tests worth implementing:
+308 -150
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -18,8 +19,9 @@
# whatever filehandle is conversing with the client and starts
# relaying data. It uses Ctrl-] like telnet for escape back to prompt
#we track nodes that are actively being logged, watched, or have attached
#there should be no more than one handler per node
# we track nodes that are actively being logged, watched, or have attached
# there should be no more than one handler per node
import codecs
import collections
import confluent.config.configmanager as configmodule
import confluent.exceptions as exc
@@ -29,32 +31,134 @@ import confluent.core as plugin
import confluent.util as util
import eventlet
import eventlet.event
import pyte
import random
import time
import traceback
_handled_consoles = {}
_genwatchattribs = frozenset(('console.method', 'console.logging'))
_tracelog = None
try:
range = xrange
except NameError:
pass
pytecolors2ansi = {
'black': 0,
'red': 1,
'green': 2,
'brown': 3,
'blue': 4,
'magenta': 5,
'cyan': 6,
'white': 7,
'default': 9,
}
# might be able to use IBMPC map from pyte charsets,
# in that case, would have to mask out certain things (like ESC)
# in the same way that Screen's draw method would do
# for now at least get some of the arrows in there (note ESC is one
# of those arrows... so skip it...
ansichars = dict(zip((0x18, 0x19), u'\u2191\u2193'))
def _utf8_normalize(data, shiftin, decoder):
# first we give the stateful decoder a crack at the byte stream,
# we may come up empty in the event of a partial multibyte
try:
data = decoder.decode(data)
except UnicodeDecodeError:
# first order of business is to reset the state of
# the decoder to a clean one, so we can switch back to utf-8
# when things change, for example going from an F1 setup menu stuck
# in the old days to a modern platform using utf-8
decoder.setstate(codecs.getincrementaldecoder('utf-8')().getstate())
# Ok, so we have something that is not valid UTF-8,
# our next stop is to try CP437. We don't try incremental
# decode, since cp437 is single byte
# replace is silly here, since there does not exist invalid c437,
# but just in case
data = data.decode('cp437', 'replace')
# Finally, the low part of ascii is valid utf-8, but we are going to be
# more interested in the cp437 versions (since this is console *output*
# not input
if shiftin is None:
data = data.translate(ansichars)
return data.encode('utf-8')
def pytechars2line(chars, maxlen=None):
line = '\x1b[m' # start at default params
lb = False # last bold
li = False # last italic
lu = False # last underline
ls = False # last strikethrough
lr = False # last reverse
lfg = 'default' # last fg color
lbg = 'default' # last bg color
hasdata = False
len = 1
for charidx in range(maxlen):
char = chars[charidx]
csi = []
if char.fg != lfg:
csi.append(30 + pytecolors2ansi[char.fg])
lfg = char.fg
if char.bg != lbg:
csi.append(40 + pytecolors2ansi[char.bg])
lbg = char.bg
if char.bold != lb:
lb = char.bold
csi.append(1 if lb else 22)
if char.italics != li:
li = char.italics
csi.append(3 if li else 23)
if char.underscore != lu:
lu = char.underscore
csi.append(4 if lu else 24)
if char.strikethrough != ls:
ls = char.strikethrough
csi.append(9 if ls else 29)
if char.reverse != lr:
lr = char.reverse
csi.append(7 if lr else 27)
if csi:
line += b'\x1b[' + b';'.join(['{0}'.format(x) for x in csi]) + b'm'
if not hasdata and char.data.encode('utf-8').rstrip():
hasdata = True
line += char.data.encode('utf-8')
if maxlen and len >= maxlen:
break
len += 1
return line, hasdata
class ConsoleHandler(object):
_plugin_path = '/nodes/{0}/_console/session'
_logtobuffer = True
_genwatchattribs = frozenset(('console.method', 'console.logging'))
class _ConsoleHandler(object):
def __init__(self, node, configmanager):
self.clearpending = False
self._dologging = True
self._isondemand = False
self.error = None
self.rcpts = {}
self._retrytime = 0
self.cfgmgr = configmanager
self.node = node
self.connectstate = 'unconnected'
self.clientcount = 0
self._isalive = True
self.logger = log.Logger(node, console=True,
tenant=configmanager.tenant)
self.buffer = bytearray()
(text, termstate, timestamp) = self.logger.read_recent_text(8192)
self.buffer = pyte.Screen(100, 31)
self.termstream = pyte.ByteStream()
self.termstream.attach(self.buffer)
self.livesessions = set([])
self.utf8decoder = codecs.getincrementaldecoder('utf-8')()
if self._logtobuffer:
self.logger = log.Logger(node, console=True,
tenant=configmanager.tenant)
(text, termstate, timestamp) = (b'', 0, False)
# when reading from log file, we will use wall clock
# it should usually match walltime.
self.lasttime = 0
@@ -66,7 +170,7 @@ class _ConsoleHandler(object):
# wall clock has gone backwards, use current time as best
# guess
self.lasttime = util.monotonic_time()
self.buffer += text
self.clearbuffer()
self.appmodedetected = False
self.shiftin = None
self.reconnect = None
@@ -79,12 +183,34 @@ class _ConsoleHandler(object):
self._console = None
self.connectionthread = None
self.send_break = None
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), _genwatchattribs, self._attribschanged)
if self._genwatchattribs:
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), self._genwatchattribs, self._attribschanged)
self.check_isondemand()
if not self._isondemand:
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
def _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)
except StopIteration: # corrupt parser state, start over
self.termstream = pyte.ByteStream()
self.termstream.attach(self.buffer)
except Exception:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def check_isondemand(self):
self._dologging = True
attrvalue = self.cfgmgr.get_node_attributes(
@@ -93,11 +219,12 @@ class _ConsoleHandler(object):
self._isondemand = False
elif 'console.logging' not in attrvalue[self.node]:
self._isondemand = False
elif (attrvalue[self.node]['console.logging']['value'] not in (
'full', '')):
self._isondemand = True
elif (attrvalue[self.node]['console.logging']['value']) == 'none':
self._dologging = False
else:
if (attrvalue[self.node]['console.logging']['value'] not in (
'full', '', 'buffer')):
self._isondemand = True
if (attrvalue[self.node]['console.logging']['value']) in ('none', 'memory'):
self._dologging = False
def get_buffer_age(self):
"""Return age of buffered data
@@ -132,9 +259,9 @@ class _ConsoleHandler(object):
return
else:
self._ondemand()
if logvalue == 'none':
if logvalue in ('none', 'memory'):
self._dologging = False
if not self._isondemand or self.clientcount > 0:
if not self._isondemand or self.livesessions:
eventlet.spawn(self._connect)
def log(self, *args, **kwargs):
@@ -151,10 +278,18 @@ class _ConsoleHandler(object):
else:
self._console.ping()
def clearbuffer(self):
self.feedbuffer(
'\x1bc[no replay buffer due to console.logging attribute set to '
'none or interactive,\r\nconnection loss, or service restart]')
self.clearpending = True
def _disconnect(self):
if self.connectionthread:
self.connectionthread.kill()
self.connectionthread = None
# clear the terminal buffer when disconnected
self.clearbuffer()
if self._console:
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
@@ -166,7 +301,7 @@ class _ConsoleHandler(object):
def _ondemand(self):
self._isondemand = True
if self.clientcount < 1 and self._console:
if not self.livesessions and self._console:
self._disconnect()
def _connect(self):
@@ -186,12 +321,15 @@ class _ConsoleHandler(object):
self.reconnect = None
try:
self._console = plugin.handle_path(
"/nodes/%s/_console/session" % self.node,
self._plugin_path.format(self.node),
"create", self.cfgmgr)
except (exc.NotImplementedException, exc.NotFoundException):
self._console = None
except:
_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,
@@ -202,39 +340,43 @@ class _ConsoleHandler(object):
self.cfgmgr.remove_watcher(self._attribwatcher)
self._attribwatcher = None
if hasattr(self._console, "configattributes"):
attribstowatch = self._console.configattributes | _genwatchattribs
attribstowatch = self._console.configattributes | self._genwatchattribs
else:
attribstowatch = _genwatchattribs
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
attribstowatch = self._genwatchattribs
if self._genwatchattribs:
self._attribwatcher = self.cfgmgr.watch_attributes(
(self.node,), attribstowatch, self._attribschanged)
try:
self._console.connect(self.get_console_output)
except exc.TargetEndpointBadCredentials:
self.clearbuffer()
self.error = 'badcredentials'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
except exc.TargetEndpointUnreachable:
self.clearbuffer()
self.error = 'unreachable'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
except Exception:
self.clearbuffer()
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
self.error = 'unknown'
self.connectstate = 'unconnected'
self._send_rcpts({'connectstate': self.connectstate,
'error': self.error})
retrytime = 30 + (30 * random.random())
retrytime = self._get_retry_time()
if not self.reconnect:
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
return
@@ -242,19 +384,23 @@ 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.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._send_rcpts({'connectstate': self.connectstate})
if self.connectstate != 'unconnected':
self.connectstate = 'unconnected'
self.log(
logdata='console disconnected', ltype=log.DataTypes.event,
event=log.Events.consoledisconnect)
self._send_rcpts({'connectstate': self.connectstate})
if self._isalive:
self._connect()
else:
self.clearbuffer()
def close(self):
self._isalive = False
@@ -268,21 +414,22 @@ class _ConsoleHandler(object):
self.connectionthread.kill()
self.connectionthread = None
def unregister_rcpt(self, handle):
self.clientcount -= 1
if handle in self.rcpts:
del self.rcpts[handle]
self._send_rcpts({'clientcount': self.clientcount})
if self._isondemand and self.clientcount < 1:
self._disconnect()
def get_console_output(self, data):
# Spawn as a greenthread, return control as soon as possible
# to the console object
eventlet.spawn(self._handle_console_output, data)
def register_rcpt(self, callback):
self.clientcount += 1
self._send_rcpts({'clientcount': self.clientcount})
hdl = random.random()
while hdl in self.rcpts:
hdl = random.random()
self.rcpts[hdl] = callback
def attachsession(self, session):
edata = 1
for currsession in self.livesessions:
if currsession.username == session.username:
# indicate that user has multiple connections
edata = 2
self.livesessions.add(session)
self.log(
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
if self.connectstate == 'unconnected':
# if console is not connected, take time to try to assert
# connectivity now.
@@ -292,52 +439,24 @@ class _ConsoleHandler(object):
self.reconnect = None
self.connectstate = 'connecting'
eventlet.spawn(self._connect)
return hdl
def flushbuffer(self):
# Logging is handled in a different stream
# this buffer is now just for having screen redraw on
# connect
self.buffer = bytearray(self.buffer[-8192:])
def get_console_output(self, data):
# Spawn as a greenthread, return control as soon as possible
# to the console object
eventlet.spawn(self._handle_console_output, data)
def attachuser(self, username):
if username in self.users:
self.users[username] += 1
else:
self.users[username] = 1
edata = self.users[username]
if edata > 2: # for log purposes, only need to
# clearly indicate redundant connections
# not connection count
edata = 2
if edata < 0:
_tracelog.log('client count negative' + traceback.format_exc(),
ltype=log.DataTypes.event,
event=log.Events.stacktrace)
edata = 0
def detachsession(self, session):
edata = 0
self.livesessions.discard(session)
for currsession in self.livesessions:
if currsession.username == session.username:
edata += 1
if edata > 1: # don't bother counting beyond 2 in the log
break
self.log(
logdata=username, ltype=log.DataTypes.event,
event=log.Events.clientconnect, eventdata=edata)
def detachuser(self, username):
self.users[username] -= 1
if self.users[username] < 2:
edata = self.users[username]
else:
edata = 2
if edata < 0:
_tracelog.log('client count negative' + traceback.format_exc(),
ltype=log.DataTypes.event,
event=log.Events.stacktrace)
edata = 0
self.log(
logdata=username, ltype=log.DataTypes.event,
logdata=session.username, ltype=log.DataTypes.event,
event=log.Events.clientdisconnect, eventdata=edata)
self._send_rcpts({'clientcount': len(self.livesessions)})
if self._isondemand and not self.livesessions:
self._disconnect()
def reopen(self):
self._got_disconnected()
@@ -365,65 +484,73 @@ class _ConsoleHandler(object):
eventdata |= 2
self.log(data, eventdata=eventdata)
self.lasttime = util.monotonic_time()
if isinstance(data, bytearray) or isinstance(data, bytes):
self.buffer += data
else:
self.buffer += data.encode('utf-8')
#TODO: analyze buffer for registered events, examples:
self.feedbuffer(data)
# TODO: analyze buffer for registered events, examples:
# panics
# certificate signing request
if len(self.buffer) > 16384:
self.flushbuffer()
self._send_rcpts(data)
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 self.rcpts.itervalues():
for rcpt in list(self.livesessions):
try:
rcpt(data)
rcpt.data_handler(data)
except: # No matter the reason, advance to next recipient
pass
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
def get_recent(self):
"""Retrieve 'recent' data
Replay data in the intent to perhaps reproduce the display.
"""
#For now, just try to seek back in buffer to find a clear screen
#If that fails, just return buffer
#a scheme always tracking the last clear screen would be too costly
# For now, just try to seek back in buffer to find a clear screen
# If that fails, just return buffer
# a scheme always tracking the last clear screen would be too costly
connstate = {
'connectstate': self.connectstate,
'clientcount': self.clientcount,
'clientcount': len(self.livesessions),
}
retdata = ''
retdata = b'\x1b[H\x1b[J' # clear screen
pendingbl = b'' # pending blank lines
maxlen = 0
for line in self.buffer.display:
line = line.rstrip()
if len(line) > maxlen:
maxlen = len(line)
for line in range(self.buffer.lines):
nline, notblank = pytechars2line(self.buffer.buffer[line], maxlen)
if notblank:
if pendingbl:
retdata += pendingbl
pendingbl = b''
retdata += nline + '\r\n'
else:
pendingbl += nline + '\r\n'
if len(retdata) > 6:
retdata = retdata[:-2] # remove the last \r\n
retdata += b'\x1b[{0};{1}H'.format(self.buffer.cursor.y + 1,
self.buffer.cursor.x + 1)
if self.shiftin is not None: # detected that terminal requested a
#shiftin character set, relay that to the terminal that cannected
# shiftin character set, relay that to the terminal that cannected
retdata += '\x1b)' + self.shiftin
if self.appmodedetected:
retdata += '\x1b[?1h'
else:
retdata += '\x1b[?1l'
#an alternative would be to emulate a VT100 to know what the
#whole screen would look like
#this is one scheme to clear screen, move cursor then clear
bufidx = self.buffer.rfind('\x1b[H\x1b[J')
if bufidx >= 0:
return retdata + str(self.buffer[bufidx:]), connstate
#another scheme is the 2J scheme
bufidx = self.buffer.rfind('\x1b[2J')
if bufidx >= 0:
# there was some sort of clear screen event
# somewhere in the buffer, replay from that point
# in hopes that it reproduces the screen
return retdata + str(self.buffer[bufidx:]), connstate
else:
#we have no indication of last erase, play back last kibibyte
#to give some sense of context anyway
return retdata + str(self.buffer[-1024:]), connstate
return retdata, connstate
def write(self, data):
if self.connectstate == 'connected':
self._console.write(data)
try:
self._console.write(data)
except Exception:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
self._got_disconnected()
def disconnect_node(node, configmanager):
@@ -456,13 +583,15 @@ def start_console_sessions():
configmodule.hook_new_configmanagers(_start_tenant_sessions)
def connect_node(node, configmanager):
def connect_node(node, configmanager, username=None):
consk = (node, configmanager.tenant)
if consk not in _handled_consoles:
_handled_consoles[consk] = _ConsoleHandler(node, configmanager)
#this represents some api view of a console handler. This handles things like
#holding the caller specific queue data, for example, when http api should be
#sending data, but there is no outstanding POST request to hold it,
_handled_consoles[consk] = ConsoleHandler(node, configmanager)
return _handled_consoles[consk]
# this represents some api view of a console handler. This handles things like
# holding the caller specific queue data, for example, when http api should be
# sending data, but there is no outstanding POST request to hold it,
# this object has the job of holding the data
@@ -474,7 +603,14 @@ class ConsoleSession(object):
event watching will all be handled seamlessly
:param node: Name of the node for which this session will be created
:param configmanager: A configuration manager object for current context
:param username: Username for which this session object will operate
:param datacallback: An asynchronous data handler, to be called when data
is available. Note that if passed, it makes
'get_next_output' non-functional
:param skipreplay: If true, will skip the attempt to redraw the screen
"""
connector = connect_node
def __init__(self, node, configmanager, username, datacallback=None,
skipreplay=False):
@@ -482,30 +618,41 @@ class ConsoleSession(object):
self.tenant = configmanager.tenant
if not configmanager.is_node(node):
raise exc.NotFoundException("Invalid node")
consk = (node, self.tenant)
self.ckey = consk
self.username = username
connect_node(node, configmanager)
self.node = node
self.configmanager = configmanager
self.connect_session()
self.registered = True
_handled_consoles[consk].attachuser(username)
self._evt = None
self.node = node
self.conshdl = _handled_consoles[consk]
self.write = _handled_consoles[consk].write
self.write = self.conshdl.write
if datacallback is None:
self.reaper = eventlet.spawn_after(15, self.destroy)
self.databuffer = collections.deque([])
self.reghdl = _handled_consoles[consk].register_rcpt(self.got_data)
self.data_handler = self.got_data
if not skipreplay:
self.databuffer.extend(_handled_consoles[consk].get_recent())
self.databuffer.extend(self.conshdl.get_recent())
else:
self.reghdl = _handled_consoles[consk].register_rcpt(datacallback)
self.data_handler = datacallback
if not skipreplay:
for recdata in _handled_consoles[consk].get_recent():
for recdata in self.conshdl.get_recent():
if recdata:
datacallback(recdata)
self.conshdl.attachsession(self)
def connect_session(self):
"""Connect to the appropriate backend handler
This is not intended to be called by your usual consumer,
it is a hook for confluent to abstract the concept of a terminal
between console and shell.
"""
self.conshdl = connect_node(self.node, self.configmanager,
self.username)
def send_break(self):
"""Send break to remote system
"""
self.conshdl.send_break()
def get_buffer_age(self):
@@ -515,13 +662,21 @@ class ConsoleSession(object):
return self.conshdl.get_buffer_age()
def reopen(self):
"""Reopen the session
This can be useful if there is suspicion that the remote console is
dead. Note that developers should consider need for this a bug unless
there really is some fundamental, unavoidable limitation regarding
automatically detecting an unusable console in the underlying
technology that cannot be unambiguously autodetected.
"""
self.conshdl.reopen()
def destroy(self):
if self.registered:
_handled_consoles[self.ckey].detachuser(self.username)
_handled_consoles[self.ckey].unregister_rcpt(self.reghdl)
self.databuffer = None
self.conshdl.detachsession(self)
if self._evt:
self._evt.send()
self._evt = None
self.reghdl = None
@@ -529,19 +684,26 @@ class ConsoleSession(object):
"""Receive data from console and buffer
If the caller does not provide a callback and instead will be polling
for data, we must maintain data in a buffer until retrieved
for data, we must maintain data in a buffer until retrieved. This is
an internal function used as a means to convert the async behavior to
polling for consumers that cannot do the async behavior.
"""
self.databuffer.append(data)
if self._evt:
self._evt.send()
self._evt = None
def get_next_output(self, timeout=45):
"""Poll for next available output on this console.
Ideally purely event driven scheme is perfect. AJAX over HTTP is
at least one case where we don't have that luxury
at least one case where we don't have that luxury. This function
will not work if the session was initialized with a data callback
instead of polling mode.
"""
self.reaper.cancel()
# postpone death to be 15 seconds after this would timeout
self.reaper = eventlet.spawn_after(timeout + 15, self.destroy)
if self._evt:
raise Exception('get_next_output is not re-entrant')
if not self.databuffer:
@@ -550,16 +712,12 @@ class ConsoleSession(object):
self._evt.wait()
self._evt = None
if not self.databuffer:
self.reaper = eventlet.spawn_after(15, self.destroy)
return ""
currdata = self.databuffer.popleft()
if isinstance(currdata, dict):
self.reaper = eventlet.spawn_after(15, self.destroy)
return currdata
retval = currdata
while self.databuffer and not isinstance(self.databuffer[0], dict):
retval += self.databuffer.popleft()
# the client has 15 seconds to make a new request for data before
# they are given up on
self.reaper = eventlet.spawn_after(15, self.destroy)
return retval
+296 -131
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -33,11 +33,14 @@
# functions. Console is special and just get's passed through
# see API.txt
import confluent
import confluent.alerts as alerts
import confluent.config.attributes as attrscheme
import confluent.discovery.core as disco
import confluent.interface.console as console
import confluent.exceptions as exc
import confluent.messages as msg
import confluent.networking.macmap as macmap
import confluent.noderange as noderange
try:
import confluent.shellmodule as shellmodule
@@ -70,6 +73,7 @@ def nested_lookup(nestdict, key):
def load_plugins():
# To know our plugins directory, we get the parent path of 'bin'
_init_core()
path = os.path.dirname(os.path.realpath(__file__))
plugintop = os.path.realpath(os.path.join(path, 'plugins'))
plugins = set()
@@ -77,7 +81,7 @@ def load_plugins():
plugindir = os.path.join(plugintop, plugindir)
if not os.path.isdir(plugindir):
continue
sys.path.append(plugindir)
sys.path.insert(1, plugindir)
# two passes, to avoid adding both py and pyc files
for plugin in os.listdir(plugindir):
if plugin.startswith('.'):
@@ -95,9 +99,12 @@ def load_plugins():
pluginmap[name] = tmpmod
else:
pluginmap[plugin] = tmpmod
# restore path to not include the plugindir
sys.path.pop(1)
rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/', 'events/']
rootcollections = ['discovery/', 'events/', 'networking/',
'noderange/', 'nodes/', 'nodegroups/', 'users/', 'version']
class PluginRoute(object):
@@ -109,147 +116,209 @@ class PluginCollection(object):
def __init__(self, routedict):
self.routeinfo = routedict
# _ prefix indicates internal use (e.g. special console scheme) and should not
# be enumerated in any collection
noderesources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
'boot': {
'nextdevice': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'configuration': {
'management_controller': {
'alerts': {
'destinations': PluginCollection({
def _init_core():
global noderesources
global nodegroupresources
import confluent.shellserver as shellserver
# _ prefix indicates internal use (e.g. special console scheme) and should not
# be enumerated in any collection
noderesources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
'expression': PluginRoute({'handler': 'attributes'}),
},
'boot': {
'nextdevice': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'configuration': {
'management_controller': {
'alerts': {
'destinations': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'users': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'reset': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'identifier': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'domain_name': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'ntp': {
'enabled': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'servers': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'system': {
'all': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'users': PluginCollection({
},
'_console': {
'session': PluginRoute({
'pluginattrs': ['console.method'],
}),
},
'_shell': {
'session': PluginRoute({
# For now, not configurable, wait until there's demand
'handler': 'ssh',
}),
},
'_enclosure': {
'reseat_bay': PluginRoute(
{'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi'}),
},
'shell': {
# another special case similar to console
'sessions': PluginCollection({
'handler': shellserver,
}),
},
'console': {
# this is a dummy value, http or socket must handle special
'session': None,
'license': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'net_interfaces': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'reset': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'identifier': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'domain_name': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'ntp': {
'enabled': PluginRoute({
},
'events': {
'hardware': {
'log': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'servers': PluginCollection({
'decode': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
}
},
'_console': {
'session': PluginRoute({
'pluginattrs': ['console.method'],
}),
},
'console': {
# this is a dummy value, http or socket must handle special
'session': PluginRoute({}),
'license': PluginRoute({
},
#'forward': {
# # Another dummy value, currently only for the gui
# 'web': None,
#},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'identify': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'events': {
'hardware': {
'log': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'decode': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'inventory': {
'hardware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'firmware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'updates': {
'active': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
},
},
'health': {
'hardware': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'identify': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'inventory': {
'hardware': {
'all': PluginCollection({
'media': {
'uploads': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'firmware': {
'all': PluginCollection({
'attach': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
'power': {
'state': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
'sensors': {
'hardware': {
'all': PluginCollection({
'detach': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'current': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'power': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'fans': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'leds': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
},
}
},
'power': {
'state': PluginRoute({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'reseat': PluginRoute({'handler': 'enclosure'}),
},
'sensors': {
'hardware': {
'all': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'energy': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'temperature': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'power': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'fans': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
'leds': PluginCollection({
'pluginattrs': ['hardwaremanagement.method'],
'default': 'ipmi',
}),
},
nodegroupresources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
}
},
}
nodegroupresources = {
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
},
}
def create_user(inputdata, configmanager):
@@ -302,10 +371,12 @@ def iterate_collections(iterable, forcecollection=True):
def iterate_resources(fancydict):
for resource in fancydict.iterkeys():
for resource in fancydict:
if resource.startswith("_"):
continue
if not isinstance(fancydict[resource], PluginRoute): # a resource
if resource == 'abbreviate':
pass
elif not isinstance(fancydict[resource], PluginRoute): # a resource
resource += '/'
yield msg.ChildCollection(resource)
@@ -324,11 +395,14 @@ def delete_nodegroup_collection(collectionpath, configmanager):
raise Exception("Not implemented")
def delete_node_collection(collectionpath, configmanager):
def delete_node_collection(collectionpath, configmanager, isnoderange):
if len(collectionpath) == 2: # just node
node = collectionpath[-1]
configmanager.del_nodes([node])
yield msg.DeletedResource(node)
nodes = [collectionpath[-1]]
if isnoderange:
nodes = noderange.NodeRange(nodes[0], configmanager).nodes
configmanager.del_nodes(nodes)
for node in nodes:
yield msg.DeletedResource(node)
else:
raise Exception("Not implemented")
@@ -336,7 +410,8 @@ def delete_node_collection(collectionpath, configmanager):
def enumerate_nodegroup_collection(collectionpath, configmanager):
nodegroup = collectionpath[1]
if not configmanager.is_nodegroup(nodegroup):
raise exc.NotFoundException("Invalid element requested")
raise exc.NotFoundException(
'Invalid nodegroup: {0} not found'.format(nodegroup))
del collectionpath[0:2]
collection = nested_lookup(nodegroupresources, collectionpath)
return iterate_resources(collection)
@@ -356,6 +431,7 @@ def enumerate_node_collection(collectionpath, configmanager):
collection = nested_lookup(noderesources, collectionpath[2:])
if len(collectionpath) == 2 and collectionpath[0] == 'noderange':
collection['nodes'] = {}
collection['abbreviate'] = {}
if not isinstance(collection, dict):
raise exc.NotFoundException("Invalid element requested")
return iterate_resources(collection)
@@ -372,6 +448,7 @@ def create_group(inputdata, configmanager):
configmanager.add_group_attributes(attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
yield msg.CreatedResource(groupname)
def create_node(inputdata, configmanager):
@@ -385,6 +462,25 @@ def create_node(inputdata, configmanager):
configmanager.add_node_attributes(attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
yield msg.CreatedResource(nodename)
def create_noderange(inputdata, configmanager):
try:
noder = inputdata['name']
del inputdata['name']
attribmap = {}
for node in noderange.NodeRange(noder).nodes:
attribmap[node] = inputdata
except KeyError:
raise exc.InvalidArgumentException('name not specified')
try:
configmanager.add_node_attributes(attribmap)
except ValueError as e:
raise exc.InvalidArgumentException(str(e))
for node in attribmap:
yield msg.CreatedResource(node)
def enumerate_collections(collections):
@@ -399,7 +495,7 @@ def handle_nodegroup_request(configmanager, inputdata,
if len(pathcomponents) < 2:
if operation == "create":
inputdata = msg.InputAttributes(pathcomponents, inputdata)
create_group(inputdata.attribs, configmanager)
return create_group(inputdata.attribs, configmanager)
allgroups = list(configmanager.get_groups())
try:
allgroups.sort(key=noderange.humanify_nodename)
@@ -438,6 +534,26 @@ def handle_nodegroup_request(configmanager, inputdata,
raise Exception("unknown case encountered")
class BadPlugin(object):
def __init__(self, node, plugin):
self.node = node
self.plugin = plugin
def error(self, *args, **kwargs):
yield msg.ConfluentNodeError(
self.node, self.plugin + ' is not a supported plugin')
def abbreviate_noderange(configmanager, inputdata, operation):
if operation != 'create':
raise exc.InvalidArgumentException('Must be a create with nodes in list')
if 'nodes' not in inputdata:
raise exc.InvalidArgumentException('Must be given list of nodes under key named nodes')
if isinstance(inputdata['nodes'], str) or isinstance(inputdata['nodes'], unicode):
inputdata['nodes'] = inputdata['nodes'].split(',')
return (msg.KeyValueData({'noderange': noderange.ReverseNodeRange(inputdata['nodes'], configmanager).noderange}),)
def handle_node_request(configmanager, inputdata, operation,
pathcomponents, autostrip=True):
iscollection = False
@@ -450,6 +566,8 @@ def handle_node_request(configmanager, inputdata, operation,
# facility anyway
isnoderange = False
pathcomponents = pathcomponents[2:]
elif len(pathcomponents) == 3 and pathcomponents[2] == 'abbreviate':
return abbreviate_noderange(configmanager, inputdata, operation)
else:
isnoderange = True
else:
@@ -458,7 +576,8 @@ def handle_node_request(configmanager, inputdata, operation,
nodeorrange = pathcomponents[1]
if not isnoderange and not configmanager.is_node(nodeorrange):
raise exc.NotFoundException("Invalid Node")
if isnoderange:
if isnoderange and not (len(pathcomponents) == 3 and
pathcomponents[2] == 'abbreviate'):
try:
nodes = noderange.NodeRange(nodeorrange, configmanager).nodes
except Exception as e:
@@ -469,11 +588,14 @@ def handle_node_request(configmanager, inputdata, operation,
# this is enumerating a list of nodes or just empty noderange
if isnoderange and operation == "retrieve":
return iterate_collections([])
elif isnoderange and operation == "create":
inputdata = msg.InputAttributes(pathcomponents, inputdata)
return create_noderange(inputdata.attribs, configmanager)
elif isnoderange or operation == "delete":
raise exc.InvalidArgumentException()
if operation == "create":
inputdata = msg.InputAttributes(pathcomponents, inputdata)
create_node(inputdata.attribs, configmanager)
return create_node(inputdata.attribs, configmanager)
allnodes = list(configmanager.list_nodes())
try:
allnodes.sort(key=noderange.humanify_nodename)
@@ -500,9 +622,12 @@ def handle_node_request(configmanager, inputdata, operation,
iscollection = True
elif isinstance(routespec, PluginCollection):
iscollection = False # it is a collection, but plugin defined
elif routespec is None:
raise exc.InvalidArgumentException('Custom interface required for resource')
if iscollection:
if operation == "delete":
return delete_node_collection(pathcomponents, configmanager)
return delete_node_collection(pathcomponents, configmanager,
isnoderange)
elif operation == "retrieve":
return enumerate_node_collection(pathcomponents, configmanager)
else:
@@ -511,15 +636,21 @@ def handle_node_request(configmanager, inputdata, operation,
passvalues = []
plugroute = routespec.routeinfo
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata, nodes, isnoderange)
pathcomponents, operation, inputdata, nodes, isnoderange,
configmanager)
if 'handler' in plugroute: # fixed handler definition, easy enough
hfunc = getattr(pluginmap[plugroute['handler']], operation)
if isinstance(plugroute['handler'], str):
hfunc = getattr(pluginmap[plugroute['handler']], operation)
else:
hfunc = getattr(plugroute['handler'], operation)
passvalue = hfunc(
nodes=nodes, element=pathcomponents,
configmanager=configmanager,
inputdata=inputdata)
if isnoderange:
return passvalue
elif isinstance(passvalue, console.Console):
return passvalue
else:
return stripnode(passvalue, nodes[0])
elif 'pluginattrs' in plugroute:
@@ -534,7 +665,11 @@ def handle_node_request(configmanager, inputdata, operation,
if attrname in nodeattr[node]:
plugpath = nodeattr[node][attrname]['value']
if plugpath is not None:
hfunc = getattr(pluginmap[plugpath], operation)
try:
hfunc = getattr(pluginmap[plugpath], operation)
except KeyError:
nodesbyhandler[BadPlugin(node, plugpath).error] = [node]
continue
if hfunc in nodesbyhandler:
nodesbyhandler[hfunc].append(node)
else:
@@ -546,11 +681,28 @@ def handle_node_request(configmanager, inputdata, operation,
inputdata=inputdata))
if isnoderange or not autostrip:
return itertools.chain(*passvalues)
elif isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
if len(passvalues) > 0:
if isinstance(passvalues[0], console.Console):
return passvalues[0]
else:
return stripnode(passvalues[0], nodes[0])
else:
raise exc.NotImplementedException()
# elif isinstance(passvalues[0], console.Console):
# return passvalues[0]
# else:
# return stripnode(passvalues[0], nodes[0])
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
if pathcomponents[0] == 'detected':
pass
def handle_discovery(pathcomponents, operation, configmanager, inputdata):
if pathcomponents[0] == 'detected':
pass
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
"""Given a full path request, return an object.
@@ -576,6 +728,14 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
# single node request of some sort
return handle_node_request(configmanager, inputdata,
operation, pathcomponents, autostrip)
elif pathcomponents[0] == 'discovery':
return disco.handle_api_request(
configmanager, inputdata, operation, pathcomponents)
elif pathcomponents[0] == 'networking':
return macmap.handle_api_request(
configmanager, inputdata, operation, pathcomponents)
elif pathcomponents[0] == 'version':
return (msg.Attributes(kv={'version': confluent.__version__}),)
elif pathcomponents[0] == 'users':
# TODO: when non-administrator accounts exist,
# they must only be allowed to see their own user
@@ -584,7 +744,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
except IndexError: # it's just users/
if operation == 'create':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata)
pathcomponents, operation, inputdata,
configmanager=configmanager)
create_user(inputdata.attribs, configmanager)
return iterate_collections(configmanager.list_users(),
forcecollection=False)
@@ -596,7 +757,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
return delete_user(user, configmanager)
elif operation == 'update':
inputdata = msg.get_input_message(
pathcomponents, operation, inputdata)
pathcomponents, operation, inputdata,
configmanager=configmanager)
update_user(user, inputdata.attribs, configmanager)
return show_user(user, configmanager)
elif pathcomponents[0] == 'events':
@@ -610,5 +772,8 @@ def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
raise exc.NotFoundException()
if operation == 'update':
return alerts.decode_alert(inputdata, configmanager)
elif pathcomponents[0] == 'discovery':
return handle_discovery(pathcomponents[1:], operation, configmanager,
inputdata)
else:
raise exc.NotFoundException()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,157 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.generic as generic
import confluent.exceptions as exc
import confluent.netutil as netutil
import eventlet.support.greendns
# Provide foundation for general IPMI device configuration
import pyghmi.exceptions as pygexc
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
ipmicommand.session.select = eventlet.green.select
ipmicommand.session.threading = eventlet.green.threading
ipmicommand.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
getaddrinfo = eventlet.support.greendns.getaddrinfo
DEFAULT_USER = 'USERID'
DEFAULT_PASS = 'PASSW0RD'
class NodeHandler(generic.NodeHandler):
def _get_ipmicmd(self, user=DEFAULT_USER, password=DEFAULT_PASS):
return ipmicommand.Command(self.ipaddr, user, password)
def __init__(self, info, configmanager):
super(NodeHandler, self).__init__(info, configmanager)
def probe(self):
return
# TODO(jjohnson2): probe serial number and uuid
def config(self, nodename, reset=False):
self._bmcconfig(nodename, reset)
def _bmcconfig(self, nodename, reset=False):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
try:
ic = self._get_ipmicmd()
passwd = DEFAULT_PASS
except pygexc.IpmiException as pi:
creds = self.configmanager.get_node_attributes(
nodename,
['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'], decrypt=True)
user = creds.get(nodename, {}).get(
'secret.hardwaremanagementuser', {}).get('value', None)
havecustomcreds = False
if user is not None and user != DEFAULT_USER:
havecustomcreds = True
else:
user = DEFAULT_USER
passwd = creds.get(nodename, {}).get(
'secret.hardwaremanagementpassword', {}).get('value', None)
if passwd is not None and passwd != DEFAULT_PASS:
havecustomcreds = True
else:
passwd = DEFAULT_PASS
if havecustomcreds:
ic = self._get_ipmicmd(user, passwd)
else:
raise
currusers = ic.get_users()
lanchan = ic.get_network_channel()
userdata = ic.xraw_command(netfn=6, command=0x44, data=(lanchan,
1))
userdata = bytearray(userdata['data'])
maxusers = userdata[0] & 0b111111
enabledusers = userdata[1] & 0b111111
lockedusers = userdata[2] & 0b111111
cfg = self.configmanager
cd = cfg.get_node_attributes(
nodename, ['secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword',
'hardwaremanagement.manager'], True)
cd = cd.get(nodename, {})
if ('secret.hardwaremanagementuser' not in cd or
'secret.hardwaremanagementpassword' not in cd):
raise exc.TargetEndpointBadCredentials(
'secret.hardwaremanagementuser and/or '
'secret.hardwaremanagementpassword was not configured')
if ('hardwaremanagement.manager' in cd and
cd['hardwaremanagement.manager']['value'] and
not cd['hardwaremanagement.manager']['value'].startswith(
'fe80::')):
newip = cd['hardwaremanagement.manager']['value']
newipinfo = getaddrinfo(newip, 0)[0]
# This getaddrinfo is repeated in get_nic_config, could be
# optimized, albeit with a more convoluted api..
newip = newipinfo[-1][0]
if ':' in newip:
raise exc.NotImplementedException('IPv6 remote config TODO')
netconfig = netutil.get_nic_config(cfg, nodename, ip=newip)
plen = netconfig['prefix']
newip = '{0}/{1}'.format(newip, plen)
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}})
else:
raise exc.TargetEndpointUnreachable(
'hardwaremanagement.manager must be set to desired address')
newuser = cd['secret.hardwaremanagementuser']['value']
newpass = cd['secret.hardwaremanagementpassword']['value']
for uid in currusers:
if currusers[uid]['name'] == newuser:
# Use existing account that has been created
newuserslot = uid
break
else:
newuserslot = lockedusers + 1
if newuserslot < 2:
newuserslot = 2
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:
if uid <= lockedusers: # we cannot delete, settle for disable
ic.disable_user(uid, 'disable')
else:
# lead with the most critical thing, removing user access
ic.set_user_access(uid, channel=None, callback=False,
link_auth=False, ipmi_msg=False,
privilege_level='no_access')
# next, try to disable the password
ic.set_user_password(uid, mode='disable', password=None)
# ok, now we can be less paranoid
try:
ic.user_delete(uid)
except pygexc.IpmiException as ie:
if ie.ipmicode != 0xd5: # some response to the 0xff
# name...
# the user will remain, but that is life
raise
if reset:
ic.reset_bmc()
return ic
@@ -0,0 +1,96 @@
# 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 errno
import eventlet
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]
# 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]
self.ipaddr = targsa[0]
def scan(self):
# Do completely passive things to enhance data.
# Probe is permitted to for example attempt a login
# scan *only* does what it can without a login attempt
return
def probe(self):
# Use appropriate direct strategy to gather data such as
# serial number and uuid to flesh out data as needed
return
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
def _savecert(self, certificate):
self._fp = certificate
return True
@property
def cert_fail_reason(self):
if self._certfailreason == 1:
return 'refused'
elif self._certfailreason == 2:
return 'unreachable'
@property
def https_cert(self):
if self._fp:
return self._fp
if ':' in self.ipaddr:
ip = '[{0}]'.format(self.ipaddr)
else:
ip = self.ipaddr
wc = webclient.SecureHTTPConnection(ip, verifycallback=self._savecert)
try:
wc.connect()
except IOError as ie:
if ie.errno == errno.ECONNREFUSED:
self._certfailreason = 1
return None
elif ie.errno == errno.EHOSTUNREACH:
self._certfailreason = 2
return None
self._certfailreason = 2
return None
except Exception:
self._certfailreason = 2
return None
return self._fp
@@ -0,0 +1,85 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.bmc as bmchandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.private.util as pygutil
import string
import struct
class NodeHandler(bmchandler.NodeHandler):
devname = 'IMM'
def 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()
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'
except pygexc.IpmiException as ie:
print(repr(ie))
raise
@@ -0,0 +1,39 @@
# 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 contains functionality for passive detection and, one day, active
# response to pxe
import confluent.discovery.handlers.generic as generic
class NodeHandler(generic.NodeHandler):
https_supported = False
is_enclosure = False
devname = 'PXE'
def __init__(self, info, configmanager):
self.ipaddr = ''
self.cfm = configmanager
@property
def cert_fail_reason(self):
return 'unsupported'
@property
def https_cert(self):
return None
def config(self, nodename):
return
@@ -0,0 +1,56 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.bmc as bmchandler
import struct
def fixuuid(baduuid):
# SMM dumps it out in hex
uuidprefix = (baduuid[:8], baduuid[8:12], baduuid[12:16])
a = struct.pack('<IHH', *[int(x, 16) for x in uuidprefix]).encode(
'hex')
uuid = (a[:8], a[8:12], a[12:16], baduuid[16:20], baduuid[20:])
return '-'.join(uuid).lower()
class NodeHandler(bmchandler.NodeHandler):
is_enclosure = True
devname = 'SMM'
maxmacs = 5 # support an enclosure, but try to avoid catching daisy chain
def scan(self):
# the UUID is in a weird order, fix it up to match
# ipmi return and property value
uuid = self.info.get('attributes', {}).get('uuid', None)
if uuid:
uuid = fixuuid(uuid[0])
self.info['uuid'] = uuid
def config(self, nodename):
# SMM for now has to reset to assure configuration applies
super(NodeHandler, self).config(nodename)
# notes for smm:
# POST to:
# https://172.30.254.160/data/changepwd
# oripwd=PASSW0RD&newpwd=Passw0rd!4321
# got response:
# <?xml version="1.0" encoding="UTF-8"?><root><statusCode>0-ChangePwd</statusCode><fowardUrl>login.html</fowardUrl><status>ok</status></root>
# requires relogin
# https://172.30.254.160/index.html
# post to:
# https://172.30.254.160/data/login
# with body user=USERID&password=Passw0rd!4321
# yields:
# <?xml version="1.0" encoding="UTF-8"?><root> <status>ok</status> <authResult>0</authResult> <forwardUrl>index.html</forwardUrl> </root>
# note forwardUrl, if password change needed, will indicate something else
@@ -0,0 +1,72 @@
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.discovery.handlers.imm as immhandler
import pyghmi.exceptions as pygexc
import pyghmi.ipmi.oem.lenovo.imm as imm
class NodeHandler(immhandler.NodeHandler):
devname = 'XCC'
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
try:
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)):
# raise an issue if anything other than to be expected
raise
#TODO: decide how to clean out if important
#as it stands, this can step on itself
#if ipmicmd:
# ipmicmd.ipmi_session.logout()
def config(self, nodename, reset=False):
# TODO(jjohnson2): set ip parameters, user/pass, alert cfg maybe
# In general, try to use https automation, to make it consistent
# between hypothetical secure path and today.
ic = self._bmcconfig(nodename)
ff = self.info.get('attributes', {}).get('enclosure-form-factor', '')
if ff not in ('dense-computing', [u'dense-computing']):
return
# Ok, we can get the enclosure uuid now..
ic.oem_init()
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
enclosureuuid = ic._oem.immhandler.get_property(
'/v2/ibmc/smm/chassis/uuid')
if enclosureuuid:
enclosureuuid = imm.fixup_uuid(enclosureuuid).lower()
em = self.configmanager.get_node_attributes(nodename,
'enclosure.manager')
em = em.get(nodename, {}).get('enclosure.manager', {}).get(
'value', None)
# ok, set the uuid of the manager...
if em:
self.configmanager.set_node_attributes(
{em: {'id.uuid': enclosureuuid}})
# TODO(jjohnson2): web based init config for future prevalidated cert scheme
# def config(self, nodename):
# return
@@ -0,0 +1,118 @@
# 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.
# We can listen to port 69 with SO_REUSEADDR to snoop port 69 *even* if dhcp
# is running (because the other dhcp servers do it already)
# Goal is to detect and act on a DHCPDISCOVER, without actually having to do
# any offer
# option 97 = UUID (wireformat)
import eventlet.green.socket as socket
import struct
pxearchs = {
'\x00\x00': 'bios-x86',
'\x00\x07': 'uefi-x64',
'\x00\x09': 'uefi-x64',
'\x00\x0b': 'uefi-aarch64',
}
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()
def find_info_in_options(rq, optidx):
uuid = None
arch = None
try:
while uuid is None or arch is None:
if rq[optidx] == 53: # DHCP message type
# we want only length 1 and only discover (type 1)
if rq[optidx + 1] != 1 or rq[optidx + 2] != 1:
return uuid, arch
optidx += 3
elif rq[optidx] == 97:
if rq[optidx + 1] != 17:
# 16 bytes of uuid and one reserved byte
return uuid, arch
if rq[optidx + 2] != 0: # the reserved byte should be zero,
# anything else would be a new spec that we don't know yet
return uuid, arch
uuid = decode_uuid(rq[optidx + 3:optidx + 19])
optidx += 19
elif rq[optidx] == 93:
if rq[optidx + 1] != 2:
return uuid, arch
archraw = bytes(rq[optidx + 2:optidx + 4])
if archraw in pxearchs:
arch = pxearchs[archraw]
optidx += 4
else:
optidx += rq[optidx + 1] + 2
except IndexError:
return uuid, arch
return uuid, arch
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
#proxydhcp.c from xCAT
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.bind(('', 67))
while True:
# Just need some delay, picked a prime number so that overlap with other
# timers might be reduced, though it really is probably nothing
(rq, peer) = net4.recvfrom(9000)
# if we have a small packet, just skip, it can't possible hold enough
# data and avoids some downstream IndexErrors that would be messy
# with try/except
if len(rq) < 64:
continue
rq = bytearray(rq)
if rq[0] == 1: # Boot request
addrlen = rq[2]
if addrlen > 16 or addrlen == 0:
continue
netaddr = rq[28:28+addrlen]
netaddr = ':'.join(['{0:02x}'.format(x) for x in netaddr])
optidx = 0
try:
optidx = rq.index('\x63\x82\x53\x63') + 4
except ValueError:
continue
uuid, arch = find_info_in_options(rq, optidx)
if uuid is None:
continue
# We will fill out service to have something to byte into,
# but the nature of the beast is that we do not have peers,
# so that will not be present for a pxe snoop
handler({'hwaddr': netaddr, 'uuid': uuid, 'architecture': arch,
'services': ('pxe-client',)})
if __name__ == '__main__':
def testsnoop(info):
print(repr(info))
snoop(testsnoop)
@@ -0,0 +1,545 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import confluent.neighutil as neighutil
import confluent.util as util
import os
import random
import eventlet.green.select as select
import eventlet.green.socket as socket
import struct
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.
# So we have a static footer here to always use 'DEFAULT' scope, no LDAP
# predicates, and no authentication for service requests
srvreqfooter = b'\x00\x07DEFAULT\x00\x00\x00\x00'
# An empty instance of the attribute list extension
# 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)
if len(packet) < 16 or packet[0] != 2:
# discard packets that are obviously useless
return None
parsed = {
'function': packet[1],
}
(offset, parsed['xid'], langlen) = struct.unpack('!IHH',
bytes(b'\x00' + packet[7:14]))
parsed['lang'] = packet[14:14 + langlen].decode('utf-8')
parsed['payload'] = packet[14 + langlen:]
if offset:
parsed['offset'] = 14 + langlen
parsed['extoffset'] = offset
return parsed
def _pop_url(payload):
urllen = struct.unpack('!H', bytes(payload[3:5]))[0]
url = bytes(payload[5:5+urllen]).decode('utf-8')
if payload[5+urllen] != 0:
raise Exception('Auth blocks unsupported')
payload = payload[5+urllen+1:]
return url, payload
def _parse_SrvRply(parsed):
""" Modify passed dictionary to have parsed data
:param parsed:
:return:
"""
payload = parsed['payload']
ecode, ucount = struct.unpack('!HH', bytes(payload[0:4]))
if ecode:
parsed['errorcode'] = ecode
payload = payload[4:]
parsed['urls'] = []
while ucount:
ucount -= 1
url, payload = _pop_url(payload)
parsed['urls'].append(url)
def _parse_slp_packet(packet, peer, rsps, xidmap):
parsed = _parse_slp_header(packet)
if not parsed:
return
addr = peer[0]
if '%' in addr:
addr = addr[:addr.index('%')]
mac = None
if addr in neighutil.neightable:
identifier = neighutil.neightable[addr]
mac = identifier
else:
identifier = addr
if (identifier, parsed['xid']) in rsps:
# avoid obviously duplicate entries
parsed = rsps[(identifier, parsed['xid'])]
else:
rsps[(identifier, parsed['xid'])] = parsed
if mac and 'hwaddr' not in parsed:
parsed['hwaddr'] = mac
if parsed['xid'] in xidmap:
parsed['services'] = [xidmap[parsed['xid']]]
if 'addresses' in parsed:
if peer not in parsed['addresses']:
parsed['addresses'].append(peer)
else:
parsed['addresses'] = [peer]
if parsed['function'] == 2: # A service reply
_parse_SrvRply(parsed)
def _v6mcasthash(srvtype):
# The hash algorithm described by RFC 3111
nums = bytearray(srvtype.encode('utf-8'))
hashval = 0
for i in nums:
hashval *= 33
hashval += i
hashval &= 0xffff # only need to track the lowest 16 bits
hashval &= 0x3ff
hashval |= 0x1000
return '{0:x}'.format(hashval)
def _generate_slp_header(payload, multicast, functionid, xid, extoffset=0):
if multicast:
flags = 0x2000
else:
flags = 0
packetlen = len(payload) + 16 # we have a fixed 16 byte header supported
if extoffset: # if we have an offset, add 16 to account for this function
# generating a 16 byte header
extoffset += 16
if packetlen > 1400:
# For now, we aren't intending to support large SLP transmits
# raise an exception to help identify if such a requirement emerges
raise Exception("TODO: Transmit overflow packets")
# We always do SLP v2, and only v2
header = bytearray([2, functionid])
# SLP uses 24 bit packed integers, so in such places we pack 32 then
# discard the high byte
header.extend(struct.pack('!IH', packetlen, flags)[1:])
# '2' below refers to the length of the language tag
header.extend(struct.pack('!IHH', extoffset, xid, 2)[1:])
# we only do english (in SLP world, it's not like non-english appears...)
header.extend(b'en')
return header
def _generate_attr_request(service, xid):
service = service.encode('utf-8')
payload = bytearray(struct.pack('!HH', 0, len(service)) + service)
payload.extend(srvreqfooter)
header = _generate_slp_header(payload, False, functionid=6, xid=xid)
return header + payload
def _generate_request_payload(srvtype, multicast, xid, prlist=''):
prlist = prlist.encode('utf-8')
payload = bytearray(struct.pack('!H', len(prlist)) + prlist)
srvtype = srvtype.encode('utf-8')
payload.extend(struct.pack('!H', len(srvtype)) + srvtype)
payload.extend(srvreqfooter)
extoffset = len(payload)
payload.extend(attrlistext)
header = _generate_slp_header(payload, multicast, functionid=1, xid=xid,
extoffset=extoffset)
return header + payload
def _find_srvtype(net, net4, srvtype, addresses, xid):
"""Internal function to find a single service type
Helper to do singleton requests to srvtype
:param net: Socket active
:param srvtype: Service type to do now
:param addresses: Pass through of addresses argument from find_targets
:return:
"""
if addresses is None:
data = _generate_request_payload(srvtype, True, xid)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
v6addrs = []
v6hash = _v6mcasthash(srvtype)
# do 'interface local' and 'link local'
# it shouldn't make sense, but some configurations work with interface
# local that do not work with link local
v6addrs.append(('ff01::1:' + v6hash, 427, 0, 0))
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)
for sa in v6addrs:
try:
net.sendto(data, sa)
except socket.error:
# if we hit an interface without ipv6 multicast,
# this can cause an error, skip such an interface
# case in point, 'lo'
pass
for i4 in util.list_ips():
if 'broadcast' not in i4:
continue
addr = i4['addr']
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, (bcast, 427))
def _grab_rsps(socks, rsps, interval, xidmap):
r, _, _ = select.select(socks, (), (), interval)
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
neighutil.refresh_neigh()
_parse_slp_packet(rsp, peer, rsps, xidmap)
r, _, _ = select.select(socks, (), (), interval)
def _parse_attrlist(attrstr):
attribs = {}
while attrstr:
if attrstr[0] == '(':
if ')' not in attrstr:
attribs['INCOMPLETE'] = True
return attribs
currattr = attrstr[1:attrstr.index(')')]
if '=' not in currattr: # Not allegedly kosher, but still..
currattr = currattr.decode('utf-8')
attribs[currattr] = None
else:
attrname, attrval = currattr.split('=')
attrname = attrname.decode('utf-8')
attribs[attrname] = []
for val in attrval.split(','):
try:
val = val.decode('utf-8')
except UnicodeDecodeError:
val = '*DECODEERROR*'
if val[:3] == '\\FF': # we should make this bytes
finalval = bytearray([])
for bnum in attrval[3:].split('\\'):
if bnum == '':
continue
finalval.append(int(bnum, 16))
val = finalval
if 'uuid' in attrname and len(val) == 16:
lebytes = struct.unpack_from(
'<IHH', buffer(val[:8]))
bebytes = struct.unpack_from(
'>HHI', buffer(val[8:]))
val = '{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()
attribs[attrname].append(val)
attrstr = attrstr[attrstr.index(')'):]
elif attrstr[0] == ',':
attrstr = attrstr[1:]
elif ',' in attrstr:
currattr = attrstr[:attrstr.index(',')]
attribs[currattr] = None
attrstr = attrstr[attrstr.index(','):]
else:
currattr = attrstr
attribs[currattr] = None
attrstr = None
return attribs
def _parse_attrs(data, parsed):
headinfo = _parse_slp_header(data)
if headinfo['function'] != 7 or headinfo['xid'] != parsed['xid']:
return
payload = headinfo['payload']
if struct.unpack('!H', bytes(payload[:2]))[0] != 0:
return
length = struct.unpack('!H', bytes(payload[2:4]))[0]
attrstr = bytes(payload[4:4+length])
parsed['attributes'] = _parse_attrlist(attrstr)
def _add_attributes(parsed):
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
for addr in parsed['addresses']:
if addr[0].startswith('fe80'):
target = addr
# however if no fe80 seen, roll with the first available address
if not target:
target = parsed['addresses'][0]
if len(target) == 4:
net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
net.connect(target)
except socket.error:
return
net.sendall(attrq)
rsp = net.recv(8192)
net.close()
_parse_attrs(rsp, parsed)
def query_srvtypes(target):
"""Query the srvtypes advertised by the target
:param target: A sockaddr tuple (if you get the peer info)
"""
payload = b'\x00\x00\xff\xff\x00\x07DEFAULT'
header = _generate_slp_header(payload, False, functionid=9, xid=1)
packet = header + payload
if len(target) == 2:
net = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
elif len(target) == 4:
net = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
else:
raise Exception('Unrecognized target {0}'.format(repr(target)))
tries = 3
connected = False
while tries and not connected:
tries -= 1
try:
net.connect(target)
connected = True
except socket.error:
pass
if not connected:
return [u'']
net.sendall(packet)
rs = net.recv(8192)
net.close()
parsed = _parse_slp_header(rs)
if parsed:
payload = parsed['payload']
if payload[:2] != '\x00\x00':
return
stypelen = struct.unpack('!H', bytes(payload[2:4]))[0]
stypes = payload[4:4+stypelen].decode('utf-8')
return stypes.split(',')
def rescan(handler):
known_peers = set([])
for scanned in scan():
for addr in scanned['addresses']:
ip = addr[0].partition('%')[0] # discard scope if present
if ip not in neighutil.neightable:
continue
if addr in known_peers:
break
known_peers.add(addr)
else:
handler(scanned)
def snoop(handler):
"""Watch for SLP activity
handler will be called with a dictionary of relevant attributes
:param handler:
:return:
"""
active_scan(handler)
net = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
slpg = socket.inet_pton(socket.AF_INET6, 'ff01::123')
slpg2 = socket.inet_pton(socket.AF_INET6, 'ff02::123')
for i6idx in util.list_interface_indexes():
mreq = slpg + struct.pack('=I', i6idx)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
mreq = slpg2 + struct.pack('=I', i6idx)
net.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
for i4 in util.list_ips():
if 'broadcast' not in i4:
continue
slpmcast = socket.inet_aton('239.255.255.253') + \
socket.inet_aton(i4['addr'])
try:
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
slpmcast)
except socket.error as e:
if e.errno != 98:
raise
# socket in use can occur when aliased ipv4 are encountered
net.bind(('', 427))
net4.bind(('', 427))
while True:
newmacs = set([])
r, _, _ = select.select((net, net4), (), (), 60)
# clear known_peers and peerbymacaddress
# to avoid stale info getting in...
# rely upon the select(0.2) to catch rapid fire and aggregate ip
# addresses that come close together
# calling code needs to understand deeper context, as snoop
# will now yield dupe info over time
known_peers = set([])
peerbymacaddress = {}
neighutil.update_neigh()
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
ip = peer[0].partition('%')[0]
if ip not in neighutil.neightable:
continue
if peer in known_peers:
continue
known_peers.add(peer)
mac = neighutil.neightable[ip]
if mac in peerbymacaddress:
peerbymacaddress[mac]['addresses'].append(peer)
else:
q = query_srvtypes(peer)
if not q or not q[0]:
# SLP might have started and not ready yet
# ignore for now
known_peers.discard(peer)
continue
# we want to prioritize the very well known services
svcs = []
for svc in q:
if svc in _slp_services:
svcs.insert(0, svc)
else:
svcs.append(svc)
peerbymacaddress[mac] = {
'services': svcs,
'addresses': [peer],
}
newmacs.add(mac)
r, _, _ = select.select((net, net4), (), (), 0.2)
for mac in newmacs:
peerbymacaddress[mac]['xid'] = 1
_add_attributes(peerbymacaddress[mac])
peerbymacaddress[mac]['hwaddr'] = mac
handler(peerbymacaddress[mac])
def active_scan(handler):
known_peers = set([])
for scanned in scan():
for addr in scanned['addresses']:
ip = addr[0].partition('%')[0] # discard scope if present
if ip not in neighutil.neightable:
continue
if addr in known_peers:
break
known_peers.add(addr)
else:
handler(scanned)
def scan(srvtypes=_slp_services, addresses=None, localonly=False):
"""Find targets providing matching requested srvtypes
This is a generator that will iterate over respondants to the SrvType
requested.
:param srvtypes: An iterable list of the service types to find
:param addresses: An iterable of addresses/ranges. Default is to scan
local network segment using multicast and broadcast.
Each address can be a single address, hyphen-delimited
range, or an IP/CIDR indication of a network.
:return: Iterable set of results
"""
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
# 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)
# we are going to do broadcast, so allow that...
initxid = random.randint(0, 32768)
xididx = 0
xidmap = {}
# First we give fast repsonders of each srvtype individual chances to be
# processed, mitigating volume of response traffic
rsps = {}
for srvtype in srvtypes:
xididx += 1
_find_srvtype(net, net4, srvtype, addresses, initxid + xididx)
xidmap[initxid + xididx] = srvtype
_grab_rsps((net, net4), rsps, 0.1, xidmap)
# now do a more slow check to work to get stragglers,
# but fortunately the above should have taken the brunt of volume, so
# reduced chance of many responses overwhelming receive buffer.
_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']
del rsps[id]['xid']
yield rsps[id]
if __name__ == '__main__':
def testsnoop(a):
print(repr(a))
snoop(testsnoop)
@@ -0,0 +1,232 @@
# 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.
# Documented somewhat at
# http://buildingskb.schneider-electric.com/view.php?AID=15197
# Here is the payload of an SSDP 'announce', sent to the multicast v4/v6 1900
# NOTIFY * HTTP/1.1
# HOST: 239.255.255.250:1900
# CACHE-CONTROL: max-age=1800
# AL: https://172.30.254.151:8080/redfish/v1
# SERVER: Linux/3.14.28-ltsi Redfish/1.0
# NT: urn:dmtf-org:service:redfish-rest:1
# USN: uuid:00000000-0000-0000-0005-000000000001::urn:dmtf-org:service:redfish-rest:1
# NTS: ssdp:alive
import confluent.neighutil as neighutil
import confluent.util as util
import eventlet.green.select as select
import eventlet.green.socket as socket
import struct
mcastv4addr = '239.255.255.250'
mcastv6addr = 'ff02::c'
ssdp6mcast = socket.inet_pton(socket.AF_INET6, mcastv6addr)
smsg = ('M-SEARCH * HTTP/1.1\r\n'
'HOST: {0}:1900\r\n'
'MAN: "ssdp:discover"\r\n'
'ST: {1}\r\n'
'MX: 3\r\n\r\n')
def scan(services, target=None):
for service in services:
for rply in _find_service(service, target):
yield rply
def snoop(handler, byehandler=None):
"""Watch for SSDP notify messages
The handler shall be called on any service coming online.
byehandler is called whenever a system advertises that it is departing.
If no byehandler is specified, byebye messages are ignored. The handler is
given (as possible), the mac address, a list of viable sockaddrs to reference
the peer, and the notification type (e.g.
'urn:dmtf-org:service:redfish-rest:1'
:param handler: A handler for online notifications from network
:param byehandler: Optional handler for devices going off the network
"""
# Normally, I like using v6/v4 agnostic socket. However, since we are
# dabbling in multicast wizardry here, such sockets can cause big problems,
# so we will have two distinct sockets
known_peers = set([])
net6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
for ifidx in util.list_interface_indexes():
v6grp = ssdp6mcast + struct.pack('=I', ifidx)
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, v6grp)
net6.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i4 in util.list_ips():
ssdp4mcast = socket.inet_pton(socket.AF_INET, mcastv4addr) + \
socket.inet_aton(i4['addr'])
net4.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
ssdp4mcast)
net4.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
net4.bind(('', 1900))
net6.bind(('', 1900))
peerbymacaddress = {}
while True:
newmacs = set([])
machandlers = {}
r, _, _ = select.select((net4, net6), (), (), 60)
neighutil.update_neigh()
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
rsp = rsp.split('\r\n')
method, _, _ = rsp[0].split(' ', 2)
if method == 'NOTIFY':
ip = peer[0].partition('%')[0]
if ip not in neighutil.neightable:
continue
if peer in known_peers:
continue
mac = neighutil.neightable[ip]
known_peers.add(peer)
newmacs.add(mac)
if mac in peerbymacaddress:
peerbymacaddress[mac]['peers'].append(peer)
else:
peerbymacaddress[mac] = {
'hwaddr': mac,
'peers': [peer],
}
peerdata = peerbymacaddress[mac]
for headline in rsp[1:]:
if not headline:
continue
header, _, value = headline.partition(':')
header = header.strip()
value = value.strip()
if header == 'NT':
peerdata['service'] = value
elif header == 'NTS':
if value == 'ssdp:byebye':
machandlers[mac] = byehandler
elif value == 'ssdp:alive':
machandlers[mac] = handler
r, _, _ = select.select((net4, net6), (), (), 0.1)
for mac in newmacs:
thehandler = machandlers.get(mac, None)
if thehandler:
thehandler(peerbymacaddress[mac])
def _find_service(service, target):
net4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
net6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
if target:
addrs = socket.getaddrinfo(target, 1900, 0, socket.SOCK_DGRAM)
for addr in addrs:
host = addr[4][0]
if addr[0] == socket.AF_INET:
net4.sendto(smsg.format(host, service), addr[4])
elif addr[0] == socket.AF_INET6:
host = '[{0}]'.format(host)
net6.sendto(smsg.format(host, service), addr[4])
else:
net4.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
for idx in util.list_interface_indexes():
net6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF,
idx)
try:
net6.sendto(smsg.format('[{0}]'.format(mcastv6addr), service
), (mcastv6addr, 1900, 0, 0))
except socket.error:
# ignore interfaces without ipv6 multicast causing error
pass
for i4 in util.list_ips():
if 'broadcast' not in i4:
continue
addr = i4['addr']
bcast = i4['broadcast']
net4.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF,
socket.inet_aton(addr))
net4.sendto(smsg.format(mcastv4addr, service),
(mcastv4addr, 1900))
net4.sendto(smsg.format(bcast, service), (bcast, 1900))
# SSDP by spec encourages responses to spread out over a 3 second interval
# hence we must be a bit more patient
r, _, _ = select.select((net4, net6), (), (), 4)
peerdata = {}
while r:
for s in r:
(rsp, peer) = s.recvfrom(9000)
neighutil.refresh_neigh()
_parse_ssdp(peer, rsp, peerdata)
r, _, _ = select.select((net4, net6), (), (), 4)
for nid in peerdata:
yield peerdata[nid]
def _parse_ssdp(peer, rsp, peerdata):
ip = peer[0].partition('%')[0]
nid = ip
mac = None
if ip in neighutil.neightable:
nid = neighutil.neightable[ip]
mac = nid
headlines = rsp.split('\r\n')
try:
_, code, _ = headlines[0].split(' ', 2)
except ValueError:
return
myurl = None
if code == '200':
if nid in peerdata:
peerdatum = peerdata[nid]
else:
peerdatum = {
'peers': [peer],
'hwaddr': mac,
}
peerdata[nid] = peerdatum
for headline in headlines[1:]:
if not headline:
continue
header, _, value = headline.partition(':')
header = header.strip()
value = value.strip()
if header == 'AL' or header == 'LOCATION':
myurl = value
if 'urls' not in peerdatum:
peerdatum['urls'] = [value]
elif value not in peerdatum['urls']:
peerdatum['urls'].append(value)
elif header == 'ST':
if 'services' not in peerdatum:
peerdatum['services'] = [value]
elif value not in peerdatum['services']:
peerdatum['services'].append(value)
if __name__ == '__main__':
for rsp in scan(['urn:dmtf-org:service:redfish-rest:1']):
print(repr(rsp))
def fun(a):
print(repr(a))
def byefun(a):
print('bye' + repr(a))
snoop(fun, byefun)
+65 -9
View File
@@ -1,6 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# 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.
@@ -14,50 +15,105 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import base64
import json
class ConfluentException(Exception):
pass
apierrorcode = 500
_apierrorstr = 'Unexpected Error'
def get_error_body(self):
errstr = ' - '.join((self._apierrorstr, str(self)))
return json.dumps({'error': errstr })
@property
def apierrorstr(self):
if str(self):
return self._apierrorstr + ' - ' + str(self)
return self._apierrorstr
class NotFoundException(ConfluentException):
# Something that could be construed as a name was not found
# basically, picture an http error code 404
pass
apierrorcode = 404
_apierrorstr = 'Target not found'
class InvalidArgumentException(ConfluentException):
# Something from the remote client wasn't correct
# like http code 400
pass
apierrorcode = 400
_apierrorstr = 'Bad Request'
class TargetEndpointUnreachable(ConfluentException):
# A target system was unavailable. For example, a BMC
# was unreachable. http code 504
pass
apierrorcode = 504
_apierrorstr = 'Unreachable Target'
class TargetEndpointBadCredentials(ConfluentException):
# target was reachable, but authentication/authorization
# failed
pass
apierrorcode = 502
_apierrorstr = 'Bad Credentials'
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
pass
_apierrorstr = 'Credential store locked'
class ForbiddenRequest(ConfluentException):
# The client request is not allowed by authorization engine
pass
apierrorcode = 403
_apierrorstr = 'Forbidden'
class NotImplementedException(ConfluentException):
# The current configuration/plugin is unable to perform
# the requested task. http code 501
pass
apierrorcode = 501
_apierrorstr = '501 - Not Implemented'
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
pass
_apierrorstr = 'Global configuration contains an error'
class TargetResourceUnavailable(ConfluentException):
# This is meant for scenarios like asking to read a sensor that is
# currently unavailable. This may be a persistent or transient state
apierrocode = 503
_apierrorstr = 'Target Resource Unavailable'
class PubkeyInvalid(ConfluentException):
apierrorcode = 502
_apierrorstr = '502 - Invalid certificate or key on target'
def __init__(self, text, certificate, fingerprint, attribname, event):
super(PubkeyInvalid, self).__init__(self, text)
self.fingerprint = fingerprint
self.attrname = attribname
bodydata = {'message': text,
'event': event,
'fingerprint': fingerprint,
'fingerprintfield': attribname,
'certificate': base64.b64encode(certificate)}
self.errorbody = json.dumps(bodydata)
def get_error_body(self):
return self.errorbody
class LoggedOut(ConfluentException):
apierrorcode = 401
_apierrorstr = '401 - Logged out'
def get_error_body(self):
return '{"loggedout": 1}'
@@ -0,0 +1,131 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# provide managing firmware update process and firmware repository if/when
# the time comes
import confluent.exceptions as exc
import confluent.messages as msg
import eventlet
updatesbytarget = {}
uploadsbytarget = {}
updatepool = eventlet.greenpool.GreenPool(256)
def execupdate(handler, filename, updateobj, type):
try:
if type == 'firmware':
completion = handler(filename, progress=updateobj.handle_progress,
bank=updateobj.bank)
else:
completion = handler(filename, progress=updateobj.handle_progress)
if completion is None:
completion = 'complete'
updateobj.handle_progress({'phase': completion, 'progress': 100.0})
except exc.PubkeyInvalid as pi:
errstr = 'Certificate mismatch detected, does not match value in ' \
'attribute {0}'.format(pi.attrname)
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': errstr})
except Exception as e:
updateobj.handle_progress({'phase': 'error', 'progress': 0.0,
'detail': str(e)})
class Updater(object):
def __init__(self, node, handler, filename, tenant=None, name=None,
bank=None, type='firmware'):
self.bank = bank
self.node = node
self.phase = 'initializing'
self.detail = ''
self.percent = 0.0
self.updateproc = updatepool.spawn(execupdate, handler, filename,
self, type)
if type == 'firmware':
myparty = updatesbytarget
elif type == 'mediaupload':
myparty = uploadsbytarget
if (node, tenant) not in myparty:
myparty[(node, tenant)] = {}
if name is None:
name = 1
while '{0}'.format(name) in myparty[(node, tenant)]:
name += 1
self.name = '{0}'.format(name)
myparty[(node, tenant)][self.name] = self
def handle_progress(self, progress):
self.phase = progress.get('phase', 'unknown')
self.percent = float(progress.get('progress', 100.0))
self.detail = progress.get('detail', '')
def cancel(self):
self.updateproc.kill()
@property
def progress(self):
return {'phase': self.phase, 'progress': self.percent,
'detail': self.detail}
def remove_updates(nodes, tenant, element, type='firmware'):
if len(element) < 5 and element[:2] != ['media', 'uploads']:
raise exc.InvalidArgumentException()
upid = element[-1]
if type == 'firmware':
myparty = updatesbytarget
else:
myparty = uploadsbytarget
for node in nodes:
try:
upd = myparty[(node, tenant)][upid]
except KeyError:
raise exc.NotFoundException('No active update matches request')
upd.cancel()
del myparty[(node, tenant)][upid]
yield msg.DeletedResource(
'nodes/{0}/inventory/firmware/updates/active/{1}'.format(
node, upid))
def list_updates(nodes, tenant, element, type='firmware'):
showmode = False
if type == 'mediaupload':
myparty = uploadsbytarget
verb = 'upload'
else:
myparty = updatesbytarget
verb = 'update'
if type == 'firmware':
specificlen = 4
else:
specificlen = 2
if len(element) > specificlen:
showmode = True
upid = element[-1]
for node in nodes:
if showmode:
try:
updater = myparty[(node, tenant)][upid]
except KeyError:
raise exc.NotFoundException(
'No matching {0} process found'.format(verb))
yield msg.KeyValueData(updater.progress, name=node)
else:
for updateid in myparty.get((node, tenant), {}):
yield msg.ChildCollection(updateid)
+119
View File
@@ -0,0 +1,119 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#This handles port forwarding for web interfaces on management devices
#It will also hijack port 3900 and do best effort..
import eventlet
import eventlet.green.select as select
import eventlet.green.socket as socket
forwardersbyclient = {}
relaysbysession = {}
sessionsbyip = {}
ipsbysession = {}
sockhandler = {}
vidtargetbypeer = {}
vidforwarder = None
def handle_connection(incoming, outgoing):
while True:
r, _, _ = select.select((incoming, outgoing), (), (), 60)
for mysock in r:
data = mysock.recv(32768)
if not data:
return
if mysock == incoming:
outgoing.sendall(data)
elif mysock == outgoing:
incoming.sendall(data)
def forward_port(sock, target, clientip, sessionid):
while True:
conn, cli = sock.accept()
if cli[0] != clientip:
conn.close()
continue
try:
client = socket.create_connection((target, 443))
except Exception:
conn.close()
continue
if sessionid not in relaysbysession:
relaysbysession[sessionid] = {}
relaysbysession[sessionid][eventlet.spawn(
handle_connection, conn, client)] = conn
def forward_video():
sock = eventlet.listen(('::', 3900, 0, 0), family=socket.AF_INET6)
while True:
conn, cli = sock.accept()
if cli[0] not in vidtargetbypeer or not sessionsbyip.get(cli[0], None):
conn.close()
continue
try:
vidclient = socket.create_connection((vidtargetbypeer[cli[0]],
3900))
except Exception:
conn.close()
continue
eventlet.spawn_n(handle_connection, conn, vidclient)
def close_session(sessionid):
for addr in forwardersbyclient.get(sessionid, []):
killsock = forwardersbyclient[sessionid][addr]
sockhandler[killsock].kill()
del sockhandler[killsock]
killsock.close()
if sessionid in forwardersbyclient:
del forwardersbyclient[sessionid]
for clip in ipsbysession.get(sessionid, ()):
sessionsbyip[clip].discard(sessionid)
if sessionid in ipsbysession:
del ipsbysession[sessionid]
for relay in list(relaysbysession.get(sessionid, ())):
conn = relaysbysession[sessionid][relay]
relay.kill()
conn.close()
if sessionid in relaysbysession:
del relaysbysession[sessionid]
def get_port(addr, clientip, sessionid):
global vidforwarder
if socket.getaddrinfo(clientip, 0)[0][0] == socket.AF_INET:
clientip = '::ffff:' + clientip
if sessionid not in ipsbysession:
ipsbysession[sessionid] = set([])
if clientip not in sessionsbyip:
sessionsbyip[clientip] = set([])
sessionsbyip[clientip].add(sessionid)
ipsbysession[sessionid].add(clientip)
if sessionid not in forwardersbyclient:
forwardersbyclient[sessionid] = {}
if addr not in forwardersbyclient[sessionid]:
newsock = eventlet.listen(('::', 0, 0, 0),
family=socket.AF_INET6)
forwardersbyclient[sessionid][addr] = newsock
sockhandler[newsock] = eventlet.spawn(forward_port, newsock, addr,
clientip, sessionid)
if not vidforwarder:
vidforwarder = eventlet.spawn(forward_video)
vidtargetbypeer[clientip] = addr
return forwardersbyclient[sessionid][addr].getsockname()[1]
+288 -66
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -21,16 +21,22 @@ import Cookie
import confluent.auth as auth
import confluent.config.attributes as attribs
import confluent.consoleserver as consoleserver
import confluent.forwarder as forwarder
import confluent.exceptions as exc
import confluent.log as log
import confluent.messages
import confluent.core as pluginapi
import confluent.asynchttp
import confluent.shellserver as shellserver
import confluent.tlvdata
import confluent.util as util
import copy
import eventlet
import eventlet.greenthread
import greenlet
import json
import socket
import sys
import traceback
import time
import urlparse
@@ -42,6 +48,7 @@ tlvdata = confluent.tlvdata
auditlog = None
tracelog = None
consolesessions = {}
confluent.asynchttp.set_console_sessions(consolesessions)
httpsessions = {}
opmap = {
'POST': 'create',
@@ -140,12 +147,19 @@ create_resource_functions = {
def _sessioncleaner():
while True:
currtime = time.time()
for session in httpsessions.keys():
targsessions = []
for session in httpsessions:
if httpsessions[session]['expiry'] < currtime:
del httpsessions[session]
for session in consolesessions.keys():
targsessions.append(session)
for session in targsessions:
forwarder.close_session(session)
del httpsessions[session]
targsessions = []
for session in consolesessions:
if consolesessions[session]['expiry'] < currtime:
del consolesessions[session]
targsessions.append(session)
for session in targsessions:
del consolesessions[session]
eventlet.sleep(10)
@@ -183,6 +197,69 @@ def _get_query_dict(env, reqbody, reqtype):
qdict = nqdict
return qdict
def _should_skip_authlog(env):
if ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO']):
# we should only log starting of a console
return True
if '/sessions/current/async' in env['PATH_INFO']:
# this is effectively invisible
return True
if (env['REQUEST_METHOD'] == 'GET' and
('/sensors/' in env['PATH_INFO'] or
'/health/' in env['PATH_INFO'] or
'/power/state' in env['PATH_INFO'] or
'/nodes/' == env['PATH_INFO'] or
'/sessions/current/info' == env['PATH_INFO'] or
(env['PATH_INFO'].startswith('/noderange/') and
env['PATH_INFO'].endswith('/nodes/')))):
# these are pretty innocuous, and noisy to log.
return True
return False
def _csrf_exempt(path):
# first a get of info to get CSRF key, also '/forward/web' to enable
# the popup ability to just forward
return path == '/sessions/current/info' or path.endswith('/forward/web')
def _csrf_valid(env, session):
# This could be simplified into a statement, but this is more readable
# to have it broken out
if env['REQUEST_METHOD'] == 'GET' and _csrf_exempt(env['PATH_INFO']):
# Provide a web client a safe hook to request the CSRF token
# This means that we consider GET of /sessions/current/info to be
# a safe thing to inflict via CSRF, since CORS should prevent
# hypothetical attacker from reading the data and it has no
# side effects to speak of
return True
if 'csrftoken' not in session:
# The client has not (yet) requested CSRF protection
# so we return true
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
# The client has requested CSRF countermeasures,
# oblige the request and apply a new token to the
# session
session['csrftoken'] = util.randomstring(32)
elif 'HTTP_REFERER' in env:
# If there is a referrer, make sure it stays consistent
# across the session. A change in referer is a bad thing
try:
referer = env['HTTP_REFERER'].split('/')[2]
except IndexError:
return False
if 'validreferer' not in session:
session['validreferer'] = referer
elif session['validreferer'] != referer:
return False
return True
# The session has CSRF protection enabled, only mark valid if
# the client has provided an auth token and that token matches the
# value protecting the session
return ('HTTP_CONFLUENTAUTHTOKEN' in env and
env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken'])
def _authorize_request(env, operation):
"""Grant/Deny access based on data from wsgi env
@@ -190,6 +267,7 @@ def _authorize_request(env, operation):
"""
authdata = None
name = ''
sessionid = None
cookie = Cookie.SimpleCookie()
if 'HTTP_COOKIE' in env:
#attempt to use the cookie. If it matches
@@ -197,13 +275,32 @@ def _authorize_request(env, operation):
cc.load(env['HTTP_COOKIE'])
if 'confluentsessionid' in cc:
sessionid = cc['confluentsessionid'].value
sessid = sessionid
if sessionid in httpsessions:
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if _csrf_valid(env, httpsessions[sessionid]):
if env['PATH_INFO'] == '/sessions/current/logout':
targets = []
for mythread in httpsessions[sessionid]['inflight']:
targets.append(mythread)
for mythread in targets:
eventlet.greenthread.kill(mythread)
forwarder.close_session(sessionid)
del httpsessions[sessionid]
return ('logout',)
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
if env['PATH_INFO'] == '/sessions/current/logout':
if 'HTTP_REFERER' in env:
# note that this doesn't actually do harm
# otherwise, but this way do not give appearance
# of something having a side effect if it has the smell
# of a CSRF
return {'code': 401}
return ('logout',)
name, passphrase = base64.b64decode(
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
authdata = auth.check_user_passphrase(name, passphrase, element=None)
@@ -213,14 +310,15 @@ def _authorize_request(env, operation):
while sessid in httpsessions:
sessid = util.randomstring(32)
httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90,
'skipuserobject': authdata[4]}
'skipuserobject': authdata[4],
'inflight': set([])}
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
httpsessions[sessid]['csrftoken'] = util.randomstring(32)
cookie['confluentsessionid'] = sessid
cookie['confluentsessionid']['secure'] = 1
cookie['confluentsessionid']['httponly'] = 1
cookie['confluentsessionid']['path'] = '/'
skiplog = False
if '/console/session' in env['PATH_INFO']:
skiplog = True
skiplog = _should_skip_authlog(env)
if authdata:
auditmsg = {
'user': name,
@@ -236,8 +334,12 @@ def _authorize_request(env, operation):
auditmsg['tenant'] = authdata[3]
authinfo['tenant'] = authdata[3]
auditmsg['user'] = authdata[2]
if sessid is not None:
authinfo['sessionid'] = sessid
if not skiplog:
auditlog.log(auditmsg)
if 'csrftoken' in httpsessions[sessid]:
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
return authinfo
else:
return {'code': 401}
@@ -263,7 +365,7 @@ def _pick_mimetype(env):
return 'application/json; charset=utf-8', '.json'
elif env['PATH_INFO'].endswith('.html'):
return 'text/html', '.html'
elif 'application/json' in env['HTTP_ACCEPT']:
elif 'HTTP_ACCEPT' in env and 'application/json' in env['HTTP_ACCEPT']:
return 'application/json; charset=utf-8', ''
else:
return 'text/html', ''
@@ -271,7 +373,7 @@ def _pick_mimetype(env):
def _assign_consessionid(consolesession):
sessid = util.randomstring(32)
while sessid in consolesessions.keys():
while sessid in consolesessions:
sessid = util.randomstring(32)
consolesessions[sessid] = {'session': consolesession,
'expiry': time.time() + 60}
@@ -294,6 +396,13 @@ def resourcehandler_backend(env, start_response):
"""Function to handle new wsgi requests
"""
mimetype, extension = _pick_mimetype(env)
headers = [('Content-Type', mimetype), ('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
('X-Content-Type-Options', 'nosniff'),
('Content-Security-Policy', "default-src 'self'"),
('X-XSS-Protection', '1; mode=block'), ('X-Frame-Options', 'deny'),
('Strict-Transport-Security', 'max-age=86400'),
('X-Permitted-Cross-Domain-Policies', 'none')]
reqbody = None
reqtype = None
if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
@@ -305,30 +414,74 @@ def resourcehandler_backend(env, start_response):
operation = querydict['restexplorerop']
del querydict['restexplorerop']
authorized = _authorize_request(env, operation)
if 'logout' in authorized:
start_response('200 Successful logout', headers)
yield('{"result": "200 - Successful logout"}')
return
if 'HTTP_SUPPRESSAUTHHEADER' in env or 'HTTP_CONFLUENTAUTHTOKEN' in env:
badauth = [('Content-type', 'text/plain')]
else:
badauth = [('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')]
if authorized['code'] == 401:
start_response(
'401 Authentication Required',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
start_response('401 Authentication Required', badauth)
yield 'authentication required'
return
if authorized['code'] == 403:
start_response(
'403 Forbidden',
[('Content-type', 'text/plain'),
('WWW-Authenticate', 'Basic realm="confluent"')])
start_response('403 Forbidden', badauth)
yield 'authorization failed'
return
if authorized['code'] != 200:
raise Exception("Unrecognized code from auth engine")
headers = [('Content-Type', mimetype)]
headers.extend(
("Set-Cookie", m.OutputString())
for m in authorized['cookie'].values())
cfgmgr = authorized['cfgmgr']
if '/console/session' in env['PATH_INFO']:
if (operation == 'create') and env['PATH_INFO'] == '/sessions/current/async':
pagecontent = ""
try:
for rsp in _assemble_json(
confluent.asynchttp.handle_async(
env, querydict,
httpsessions[authorized['sessionid']]['inflight'])):
pagecontent += rsp
start_response("200 OK", headers)
yield pagecontent
return
except exc.ConfluentException as e:
if e.apierrorcode == 500:
# raise generics to trigger the tracelog
raise
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
headers)
yield e.get_error_body()
elif (env['PATH_INFO'].endswith('/forward/web') and
env['PATH_INFO'].startswith('/nodes/')):
prefix, _, _ = env['PATH_INFO'].partition('/forward/web')
_, _, nodename = prefix.rpartition('/')
hm = cfgmgr.get_node_attributes(nodename, 'hardwaremanagement.manager')
targip = hm.get(nodename, {}).get(
'hardwaremanagement.manager', {}).get('value', None)
if not targip:
start_response('404 Not Found', headers)
yield 'No hardwaremanagemnet.manager defined for node'
return
funport = forwarder.get_port(targip, env['HTTP_X_FORWARDED_FOR'],
authorized['sessionid'])
host = env['HTTP_X_FORWARDED_HOST']
url = 'https://{0}:{1}/'.format(host, funport)
start_response('302', [('Location', url)])
yield 'Our princess is in another castle!'
return
elif (operation == 'create' and ('/console/session' in env['PATH_INFO'] or
'/shell/sessions/' in env['PATH_INFO'])):
#hard bake JSON into this path, do not support other incarnations
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
if '/console/session' in env['PATH_INFO']:
prefix, _, _ = env['PATH_INFO'].partition('/console/session')
shellsession = False
elif '/shell/sessions/' in env['PATH_INFO']:
prefix, _, _ = env['PATH_INFO'].partition('/shell/sessions')
shellsession = True
_, _, nodename = prefix.rpartition('/')
if 'session' not in querydict.keys() or not querydict['session']:
auditmsg = {
@@ -343,10 +496,25 @@ def resourcehandler_backend(env, start_response):
skipreplay = False
if 'skipreplay' in querydict and querydict['skipreplay']:
skipreplay = True
datacallback = None
async = None
if 'HTTP_CONFLUENTASYNCID' in env:
async = confluent.asynchttp.get_async(env, querydict)
termrel = async.set_term_relation(env)
datacallback = termrel.got_data
try:
consession = consoleserver.ConsoleSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay)
if shellsession:
consession = shellserver.ShellSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
)
else:
consession = consoleserver.ConsoleSession(
node=nodename, configmanager=cfgmgr,
username=authorized['username'], skipreplay=skipreplay,
datacallback=datacallback
)
except exc.NotFoundException:
start_response("404 Not found", headers)
yield "404 - Request Path not recognized"
@@ -355,6 +523,8 @@ def resourcehandler_backend(env, start_response):
start_response("500 Internal Server Error", headers)
return
sessid = _assign_consessionid(consession)
if async:
async.add_console_session(sessid)
start_response('200 OK', headers)
yield '{"session":"%s","data":""}' % sessid
return
@@ -369,14 +539,51 @@ def resourcehandler_backend(env, start_response):
start_response('200 OK', headers)
yield json.dumps({'session': querydict['session']})
return # client has requests to send or receive, not both...
elif 'closesession' in querydict:
consolesessions[querydict['session']]['session'].destroy()
del consolesessions[querydict['session']]
start_response('200 OK', headers)
yield '{"sessionclosed": true}'
return
elif 'action' in querydict:
if querydict['action'] == 'break':
consolesessions[querydict['session']]['session'].send_break()
elif querydict['action'] == 'reopen':
consolesessions[querydict['session']]['session'].reopen()
else:
start_response('400 Bad Request')
yield 'Unrecognized action ' + querydict['action']
return
start_response('200 OK', headers)
yield json.dumps({'session': querydict['session']})
else: # no keys, but a session, means it's hooking to receive data
sessid = querydict['session']
if sessid not in consolesessions:
start_response('400 Expired Session', headers)
yield ''
return
consolesessions[sessid]['expiry'] = time.time() + 90
outdata = consolesessions[sessid]['session'].get_next_output(
timeout=45)
# add our thread to the 'inflight' to have a hook to terminate
# a long polling request
loggedout = None
mythreadid = greenlet.getcurrent()
httpsessions[authorized['sessionid']]['inflight'].add(mythreadid)
try:
outdata = consolesessions[sessid]['session'].get_next_output(
timeout=25)
except greenlet.GreenletExit as ge:
loggedout = ge
httpsessions[authorized['sessionid']]['inflight'].discard(
mythreadid)
if sessid not in consolesessions:
start_response('400 Expired Session', headers)
yield ''
return
if loggedout is not None:
consolesessions[sessid]['session'].destroy()
start_response('401 Logged out', headers)
yield '{"loggedout": 1}'
return
bufferage = False
if 'stampsent' not in consolesessions[sessid]:
consolesessions[sessid]['stampsent'] = True
@@ -405,12 +612,23 @@ def resourcehandler_backend(env, start_response):
url = env['PATH_INFO']
url = url.replace('.json', '')
url = url.replace('.html', '')
if url == '/sessions/current/info':
start_response('200 OK', headers)
sessinfo = {'username': authorized['username']}
if 'authtoken' in authorized:
sessinfo['authtoken'] = authorized['authtoken']
yield json.dumps(sessinfo)
return
resource = '.' + url[url.rindex('/'):]
lquerydict = copy.deepcopy(querydict)
try:
hdlr = pluginapi.handle_path(url, operation,
cfgmgr, querydict)
if 'HTTP_CONFLUENTASYNCID' in env:
confluent.asynchttp.run_handler(hdlr, env)
start_response('202 Accepted', headers)
yield 'Request queued'
return
pagecontent = ""
if mimetype == 'text/html':
for datum in _assemble_html(hdlr, resource, lquerydict, url,
@@ -421,24 +639,14 @@ def resourcehandler_backend(env, start_response):
pagecontent += datum
start_response('200 OK', headers)
yield pagecontent
except exc.NotFoundException as ne:
start_response('404 Not found', headers)
yield "404 - Request path not recognized - " + str(ne)
except exc.InvalidArgumentException as e:
start_response('400 Bad Request - ' + str(e), headers)
yield '400 - Bad Request - ' + str(e)
except exc.TargetEndpointUnreachable as tu:
start_response('504 Unreachable Target', headers)
yield '504 - Unreachable Target - ' + str(tu)
except exc.TargetEndpointBadCredentials:
start_response('502 Bad Credentials', headers)
yield '502 - Bad Credentials'
except exc.LockedCredentials:
start_response('500 Locked credential store', headers)
yield '500 - Credential store locked'
except exc.NotImplementedException:
start_response('501 Not Implemented', headers)
yield '501 Not Implemented'
except exc.ConfluentException as e:
if ((not isinstance(e, exc.LockedCredentials)) and
e.apierrorcode == 500):
# raise generics to trigger the tracelog
raise
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
headers)
yield e.get_error_body()
def _assemble_html(responses, resource, querydict, url, extension):
yield '<html><head><meta charset="UTF-8"><title>' \
@@ -495,21 +703,21 @@ def _assemble_html(responses, resource, querydict, url, extension):
'</form></body></html>')
def _assemble_json(responses, resource, url, extension):
def _assemble_json(responses, resource=None, url=None, extension=None):
#NOTE(jbjohnso) I'm considering giving up on yielding bit by bit
#in json case over http. Notably, duplicate key values from plugin
#overwrite, but we'd want to preserve them into an array instead.
#the downside is that http would just always blurt it ll out at
#once and hold on to all the data in memory
links = {
'self': {"href": resource + extension},
}
if url == '/':
pass
elif resource[-1] == '/':
links['collection'] = {"href": "../" + extension}
else:
links['collection'] = {"href": "./" + extension}
links = {}
if resource is not None:
links['self'] = {"href": resource + extension}
if url == '/':
pass
elif resource[-1] == '/':
links['collection'] = {"href": "../" + extension}
else:
links['collection'] = {"href": "./" + extension}
rspdata = {}
for rsp in responses:
if isinstance(rsp, confluent.messages.LinkRelation):
@@ -533,11 +741,14 @@ def _assemble_json(responses, resource, url, extension):
for dk in rsp.iterkeys():
if dk in rspdata:
if isinstance(rspdata[dk], list):
rspdata[dk].append(rsp[dk])
if isinstance(rsp[dk], list):
rspdata[dk].extend(rsp[dk])
else:
rspdata[dk].append(rsp[dk])
else:
rspdata[dk] = [rspdata[dk], rsp[dk]]
else:
if dk == 'databynode':
if dk == 'databynode' or dk == 'asyncresponse':
# a quirk, databynode suggests noderange
# multi response. This should *always* be a list,
# even if it will be length 1
@@ -561,9 +772,20 @@ def serve(bind_host, bind_port):
#but deps are simpler without flup
#also, the potential for direct http can be handy
#todo remains unix domain socket for even http
eventlet.wsgi.server(
eventlet.listen((bind_host, bind_port, 0, 0), family=socket.AF_INET6),
resourcehandler, log=False, log_output=False, debug=False)
sock = None
while not sock:
try:
sock = eventlet.listen(
(bind_host, bind_port, 0, 0), family=socket.AF_INET6)
except socket.error as e:
if e.errno != 98:
raise
sys.stderr.write(
'Failed to open HTTP due to busy port, trying again in'
' a second\n')
eventlet.sleep(1)
eventlet.wsgi.server(sock, resourcehandler, log=False, log_output=False,
debug=False)
class HttpApi(object):
+93 -37
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -197,7 +197,7 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
self.when = conf.get_option('log', 'when').upper()
except (AttributeError):
self.when = 'D'
self.backupCount = conf.get_int_option('log', 'backup_count') or 3
self.backupCount = conf.get_int_option('log', 'backup_count') or 0
self.maxBytes = conf.get_int_option(
'log','max_bytes') or 4 * 1024 * 1024 * 1024
if self.maxBytes < 8192:
@@ -379,7 +379,7 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
def _sizeRoll(self):
self.close()
for i in range(self.sizeRollingCount, 0, -1):
for i in range(self.backupCount - 1, 0, -1):
sbfn = "%s.%d" % (self.binpath, i)
dbfn = "%s.%d" % (self.binpath, i + 1)
stfn = "%s.%d" % (self.textpath, i)
@@ -392,8 +392,6 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
if os.path.exists(dtfn):
os.remove(dtfn)
os.rename(stfn, dtfn)
# size rolling happens, add statistics count
self.sizeRollingCount += 1
dbfn = self.binpath + ".1"
dtfn = self.textpath + ".1"
if os.path.exists(dbfn):
@@ -404,8 +402,18 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
os.rename(self.textpath, dtfn)
self._deleteFilesForSizeRolling()
return dbfn, dtfn
def _deleteFilesForSizeRolling(self):
for i in range(self.sizeRollingCount, self.backupCount -1, -1):
dbfn = "%s.%d" % (self.binpath, i)
dtfn = "%s.%d" % (self.textpath, i)
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
def _timeRoll(self):
self.close()
# get the time that this sequence started at and make it a TimeTuple
@@ -424,8 +432,7 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
addend = -3600
timeTuple = time.localtime(t + addend)
# if size rolling files exist
for i in range(self.sizeRollingCount, 0, -1):
for i in range(self.backupCount - 1, 0, -1):
sbfn = "%s.%d" % ( self.binpath, i)
dbfn = "%s.%s.%d" % (
self.binpath, time.strftime(self.suffix, timeTuple),i)
@@ -441,15 +448,18 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
os.remove(dtfn)
os.rename(stfn, dtfn)
# As time rolling happens, reset statistics count
self.sizeRollingCount = 0
dbfn = self.binpath + "." + time.strftime(self.suffix, timeTuple)
odbfn = dbfn
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
odtfn = dtfn
append=1
while os.path.exists(dbfn):
dbfn = odbfn + '.{0}'.format(append)
append += 1
append=1
while os.path.exists(dtfn):
dtfn = odtfn + '.{0}'.format(append)
append += 1
if os.path.exists(self.binpath):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
@@ -489,7 +499,7 @@ class Logger(object):
False, events will be formatted like syslog:
date: message<CR>
"""
def __new__(cls, logname, console=False, tenant=None):
def __new__(cls, logname, console=False, tenant=None, buffered=True):
global _loggers
if console:
relpath = 'consoles/' + logname
@@ -500,11 +510,12 @@ class Logger(object):
else:
return object.__new__(cls)
def __init__(self, logname, console=False, tenant=None):
def __init__(self, logname, console=False, tenant=None, buffered=True):
if hasattr(self, 'initialized'):
# we are just a copy of the same object
return
self.initialized = True
self.buffered = buffered
self.filepath = confluent.config.configmanager.get_global("logdirectory")
if self.filepath is None:
if os.name == 'nt':
@@ -534,6 +545,12 @@ class Logger(object):
tstamp = entry[1]
data = entry[2]
evtdata = entry[3]
if len(data) > 65535:
# our max log entry is 65k, take only the first 65k and put
# rest back on as a continuation
entry[2] = data[65535:]
self.logentries.appendleft(entry)
data = data[:65535]
textdate = ''
if self.isconsole and ltype != 2:
textdate = time.strftime(
@@ -604,7 +621,6 @@ class Logger(object):
offsets = []
termstate = None
recenttimestamp = 0
access_last_rename = False
flock(textfile, LOCK_SH)
while binidx > 0 and currsize < size:
binidx -= 16
@@ -614,23 +630,29 @@ class Logger(object):
struct.unpack(">BBIHIBBH", recbytes)
# rolling events found.
if ltype == DataTypes.event and evtdata == Events.logrollover:
# Now, we can only find the last renamed file which logging
# the data.
if access_last_rename == False:
access_last_rename = True
else:
txtpath, bpath = parse_last_rolling_files(textfile, offset,
datalen)
if txtpath == textpath:
break
textpath, binpath = parse_last_rolling_files(textfile, offset,
datalen)
if bpath == binpath:
break
textpath = txtpath
binpath = bpath
# Rolling event detected, close the current bin file, then open
# the renamed bin file.
flock(binfile, LOCK_UN)
flock(textfile, LOCK_UN)
binfile.close()
textfile.close()
try:
binfile = open(binpath, mode='r')
textfile = open(textpath, mode='r')
except IOError:
return '', 0, 0
binfile = None
textfile = None
break
flock(binfile, LOCK_SH)
flock(textfile, LOCK_SH)
binfile.seek(0, 2)
binidx = binfile.tell()
# things have been set up for next iteration to dig to
@@ -644,22 +666,36 @@ class Logger(object):
offsets.append((offset, datalen, textpath))
if termstate is None:
termstate = eventaux
flock(binfile, LOCK_UN)
binfile.close()
try:
flock(binfile, LOCK_UN)
binfile.close()
except:
pass
textdata = ''
while offsets:
(offset, length, textpath) = offsets.pop()
if textfile is None:
textfile = open(textpath, mode='r')
flock(textfile, LOCK_SH)
if textfile.name != textpath:
flock(textfile, LOCK_UN)
textfile.close()
try:
textfile = open(textpath)
except IOError:
return '', 0, 0
textfile.seek(offset, 0)
flock(textfile, LOCK_UN)
textfile.close()
textfile = open(textpath, mode='r')
flock(textfile, LOCK_SH)
except (ValueError, IOError) as e:
break
try:
textfile.seek(offset, 0)
except ValueError:
# file was closed, settle with what we have and continue
break
textdata += textfile.read(length)
flock(textfile, LOCK_UN)
textfile.close()
try:
flock(textfile, LOCK_UN)
textfile.close()
except:
pass
if termstate is None:
termstate = 0
return textdata, termstate, recenttimestamp
@@ -703,9 +739,29 @@ class Logger(object):
else:
self.logentries.append(
[ltype, timestamp, logdata, event, eventdata])
if self.writer is None:
self.writer = eventlet.spawn_after(2, self.writedata)
if self.buffered:
if self.writer is None:
self.writer = eventlet.spawn_after(2, self.writedata)
else:
self.writedata()
def closelog(self):
self.handler.close()
self.closer = None
globaleventlog = None
tracelog = None
def log(logdata=None, ltype=None, event=0, eventdata=None):
global globaleventlog
if globaleventlog is None:
globaleventlog = Logger('events')
globaleventlog.log(logdata, ltype, event, eventdata)
def logtrace():
global tracelog
if tracelog is None:
tracelog = Logger('trace', buffered=False)
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
+3 -3
View File
@@ -27,7 +27,7 @@ __author__ = 'jjohnson2'
import confluent.config.configmanager as configmanager
import itertools
import socket
from eventlet.support import greendns
manager_to_nodemap = {}
@@ -46,7 +46,7 @@ def node_by_manager(manager):
"""
manageraddresses = []
for tmpaddr in socket.getaddrinfo(manager, None):
for tmpaddr in greendns.getaddrinfo(manager, None):
manageraddresses.append(tmpaddr[4][0])
cfm = configmanager.ConfigManager(None)
if manager in manager_to_nodemap:
@@ -67,7 +67,7 @@ def node_by_manager(manager):
if currhm in manageraddresses:
manager_to_nodemap[manager] = node
return node
for curraddr in socket.getaddrinfo(currhm, None):
for curraddr in greendns.getaddrinfo(currhm, None):
curraddr = curraddr[4][0]
if curraddr in manageraddresses:
manager_to_nodemap[manager] = node
+86 -15
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -39,17 +39,24 @@ except ImportError:
#On platforms without pwd, give up on the sockapi in general and be http
#only for now
pass
import confluent.discovery.core as disco
import eventlet
#import eventlet.backdoor as backdoor
dbgif = False
if map(int, (eventlet.__version__.split('.'))) > [0, 18]:
import eventlet.backdoor as backdoor
dbgif = True
havefcntl = True
try:
import fcntl
except ImportError:
havefcntl = False
#import multiprocessing
import gc
from greenlet import greenlet
import sys
import os
import signal
import socket
import time
import traceback
@@ -71,8 +78,8 @@ def _daemonize():
os.open(os.devnull, os.O_RDWR)
os.dup2(0, 1)
os.dup2(0, 2)
sys.stdout = log.Logger('stdout')
sys.stderr = log.Logger('stderr')
sys.stdout = log.Logger('stdout', buffered=False)
sys.stderr = log.Logger('stderr', buffered=False)
def _updatepidfile():
@@ -83,15 +90,41 @@ def _updatepidfile():
pidfile.close()
def is_running():
# Utility function for utilities to check if confluent is running
try:
pidfile = open('/var/run/confluent/pid', 'r+')
fcntl.flock(pidfile, fcntl.LOCK_SH)
pid = pidfile.read()
if pid != '':
try:
os.kill(int(pid), 0)
return pid
except OSError:
# There is no process running by that pid, must be stale
pass
fcntl.flock(pidfile, fcntl.LOCK_UN)
pidfile.close()
except IOError:
pass
return None
def _checkpidfile():
try:
pidfile = open('/var/run/confluent/pid', 'r+')
fcntl.flock(pidfile, fcntl.LOCK_EX)
pid = pidfile.read()
if pid != '':
print ('/var/run/confluent/pid exists and indicates %s is still '
'running' % pid)
sys.exit(1)
try:
os.kill(int(pid), 0)
print ('/var/run/confluent/pid exists and indicates %s is still '
'running' % pid)
sys.exit(1)
except OSError:
# There is no process running by that pid, must be stale
pass
pidfile.seek(0)
pidfile.write(str(os.getpid()))
fcntl.flock(pidfile, fcntl.LOCK_UN)
pidfile.close()
@@ -125,11 +158,22 @@ def dumptrace(signalname, frame):
ht = open('/var/log/confluent/hangtraces', 'a')
ht.write('Dumping active trace on ' + time.strftime('%X %x\n'))
ht.write(''.join(traceback.format_stack(frame)))
for o in gc.get_objects():
if not isinstance(o, greenlet):
continue
if not o:
continue
ht.write('Thread trace: ({0})\n'.format(id(o)))
ht.write(''.join(traceback.format_stack(o.gr_frame)))
ht.close()
def doexit():
if not havefcntl:
return
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass
pidfile = open('/var/run/confluent/pid')
pid = pidfile.read()
if pid == str(os.getpid()):
@@ -142,10 +186,28 @@ def _initsecurity(config):
with open(keyfile, 'r') as keyhandle:
key = keyhandle.read()
configmanager.init_masterkey(key)
# We don't want to os._exit() until sync finishes from
# init above
configmanager.ConfigManager.wait_for_sync()
def setlimits():
try:
import resource
currlimit = resource.getrlimit(resource.RLIMIT_NOFILE)
if currlimit[0] < currlimit[1]:
resource.setrlimit(
resource.RLIMIT_NOFILE, (currlimit[1], currlimit[1]))
except Exception:
pass
def run():
signal.signal(signal.SIGUSR1, dumptrace)
setlimits()
try:
signal.signal(signal.SIGUSR1, dumptrace)
except AttributeError:
pass # silly windows
if havefcntl:
_checkpidfile()
conf.init_config()
@@ -164,25 +226,34 @@ def run():
_daemonize()
if havefcntl:
_updatepidfile()
auth.init_auth()
signal.signal(signal.SIGINT, terminate)
signal.signal(signal.SIGTERM, terminate)
#TODO(jbjohnso): eventlet has a bug about unix domain sockets, this code
#works with bugs fixed
#dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
# family=socket.AF_UNIX)
#eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
if dbgif:
oumask = os.umask(0077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass # We are not expecting the file to exist
try:
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
except AttributeError:
pass # Windows...
os.umask(oumask)
http_bind_host, http_bind_port = _get_connector_config('http')
sock_bind_host, sock_bind_port = _get_connector_config('socket')
consoleserver.start_console_sessions()
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
webservice.start()
disco.start_detection()
try:
sockservice = sockapi.SockApi(sock_bind_host, sock_bind_port)
sockservice.start()
except NameError:
pass
atexit.register(doexit)
eventlet.sleep(1)
consoleserver.start_console_sessions()
while 1:
eventlet.sleep(100)
+273 -41
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015 Lenovo
# Copyright 2015-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -19,6 +19,9 @@
# Things are defined here to 'encourage' developers to coordinate information
# format. This is also how different data formats are supported
import confluent.exceptions as exc
import confluent.config.configmanager as cfm
from copy import deepcopy
from datetime import datetime
import json
valid_health_values = set([
@@ -32,10 +35,12 @@ valid_health_values = set([
def _htmlify_structure(indict):
ret = "<ul>"
if isinstance(indict, dict):
for key in indict.iterkeys():
for key in sorted(indict):
ret += "<li>{0}: ".format(key)
if type(indict[key]) in (str, unicode, float, int):
ret += str(indict[key])
elif isinstance(indict[key], datetime):
ret += indict[key].strftime('%Y-%m-%dT%H:%M:%S')
else:
ret += _htmlify_structure(indict[key])
elif isinstance(indict, list):
@@ -55,6 +60,7 @@ def _htmlify_structure(indict):
class ConfluentMessage(object):
apicode = 200
readonly = False
defaultvalue = ''
defaulttype = 'text'
@@ -71,14 +77,15 @@ class ConfluentMessage(object):
datasource = self.kvpairs
else:
datasource = {'databynode': self.kvpairs}
jsonsnippet = json.dumps(datasource, separators=(',', ':'))[1:-1]
jsonsnippet = json.dumps(datasource, sort_keys=True, separators=(',', ':'))[1:-1]
return jsonsnippet
def raw(self):
"""Return pythonic representation of the response.
Used by httpapi while assembling data prior to json serialization"""
if hasattr(self, 'stripped') and self.stripped:
if ((hasattr(self, 'stripped') and self.stripped) or
(hasattr(self, 'notnode') and self.notnode)):
return self.kvpairs
return {'databynode': self.kvpairs}
@@ -132,7 +139,10 @@ class ConfluentMessage(object):
'<input type="checkbox" name="restexplorerhonorkey" '
'value="{1}">\r').format(valtype, key, self.desc)
return snippet
if val is not None and 'value' in val:
if (isinstance(val, bool) or isinstance(val, str) or
isinstance(val, unicode)):
value = str(val)
elif val is not None and 'value' in val:
value = val['value']
if 'inheritedfrom' in val:
notes.append('Inherited from %s' % val['inheritedfrom'])
@@ -156,7 +166,7 @@ class ConfluentMessage(object):
notes.append('Inherited from %s' % val['inheritedfrom'])
value = '********'
if self.readonly:
snippet += "{0}: {1}".format(key, value)
snippet += "{0}: {1}<br>".format(key, value)
else:
snippet += (key + ":" +
'<input type="{0}" name="{1}" value="{2}" '
@@ -169,12 +179,15 @@ class ConfluentMessage(object):
class ConfluentNodeError(object):
apicode = 500
def __init__(self, node, errorstr):
self.node = node
self.error = errorstr
def raw(self):
return {'databynode': {self.node: {'error': self.error}}}
return {'databynode': {self.node: {'errorcode': self.apicode,
'error': self.error}}}
def html(self):
return self.node + ":" + self.error
@@ -185,16 +198,32 @@ class ConfluentNodeError(object):
raise Exception(self.error)
class ConfluentResourceUnavailable(ConfluentNodeError):
apicode = 503
def __init__(self, node, errstr='Unavailable'):
self.node = node
self.error = errstr
def strip_node(self, node):
raise exc.TargetResourceUnavailable()
class ConfluentTargetTimeout(ConfluentNodeError):
apicode = 504
def __init__(self, node, errstr='timeout'):
self.node = node
self.error = errstr
def strip_node(self, node):
raise exc.TargetEndpointUnreachable(self.error)
class ConfluentTargetNotFound(ConfluentNodeError):
apicode = 404
def __init__(self, node, errorstr='not found'):
self.node = node
self.error = errorstr
@@ -204,6 +233,7 @@ class ConfluentTargetNotFound(ConfluentNodeError):
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
apicode = 502
def __init__(self, node):
self.node = node
self.error = 'bad credentials'
@@ -213,9 +243,31 @@ class ConfluentTargetInvalidCredentials(ConfluentNodeError):
class DeletedResource(ConfluentMessage):
notnode = True
def __init__(self, resource):
self.kvpairs = {}
self.kvpairs = {'deleted': resource}
def strip_node(self, node):
pass
class CreatedResource(ConfluentMessage):
notnode = True
readonly = True
def __init__(self, resource):
self.kvpairs = {'created': resource}
def strip_node(self, node):
pass
class AssignedResource(ConfluentMessage):
notnode = True
readonly = True
def __init__(self, resource):
self.kvpairs = {'assigned': resource}
class ConfluentChoiceMessage(ConfluentMessage):
valid_values = set()
@@ -310,9 +362,20 @@ class ChildCollection(LinkRelation):
extension)
def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
# TODO(jjohnson2): enhance the following to support expressions:
# InputNetworkConfiguration
# InputMCI
# InputDomainName
# InputNTPServer
def get_input_message(path, operation, inputdata, nodes=None, multinode=False,
configmanager=None):
if path[0] == 'power' and path[1] == 'state' and operation != 'retrieve':
return InputPowerMessage(path, nodes, inputdata)
elif (path in (['power', 'reseat'], ['_enclosure', 'reseat_bay']) and
operation != 'retrieve'):
return InputReseatMessage(path, nodes, inputdata)
elif path == ['attributes', 'expression']:
return InputExpression(path, inputdata, nodes)
elif path[0] in ('attributes', 'users') and operation != 'retrieve':
return InputAttributes(path, inputdata, nodes)
elif path == ['boot', 'nextdevice'] and operation != 'retrieve':
@@ -336,7 +399,8 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
return InputMCI(path, nodes, inputdata)
elif (path[:4] == ['configuration', 'management_controller',
'net_interfaces', 'management'] and operation != 'retrieve'):
return InputNetworkConfiguration(path, nodes, inputdata)
return InputNetworkConfiguration(path, nodes, inputdata,
configmanager)
elif (path[:3] == ['configuration', 'management_controller', 'domain_name']
and operation != 'retrieve'):
return InputDomainName(path, nodes, inputdata)
@@ -346,9 +410,41 @@ def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
'servers'] and operation != 'retrieve' and len(path) == 5):
return InputNTPServer(path, nodes, inputdata)
elif (path[:3] == ['configuration', 'system', 'all'] and
operation != 'retrieve'):
return InputConfigChangeSet(path, inputdata, nodes, configmanager)
elif 'inventory/firmware/updates/active' in '/'.join(path) and inputdata:
return InputFirmwareUpdate(path, nodes, inputdata)
elif '/'.join(path).startswith('media/detach'):
return DetachMedia(path, nodes, inputdata)
elif '/'.join(path).startswith('media/') and inputdata:
return InputMedia(path, nodes, inputdata)
elif inputdata:
raise exc.InvalidArgumentException()
raise exc.InvalidArgumentException(
'No known input handler for request')
class InputFirmwareUpdate(ConfluentMessage):
def __init__(self, path, nodes, inputdata):
self.filename = inputdata.get('filename', inputdata.get('url', None))
self.bank = inputdata.get('bank', None)
self.nodes = nodes
class InputMedia(InputFirmwareUpdate):
# Use InputFirmwareUpdate
pass
class DetachMedia(ConfluentMessage):
def __init__(self, path, nodes, inputdata):
if 'detachall' not in inputdata:
raise exc.InvalidArgumentException('Currently only supporting'
'{"detachall": 1}')
class Media(ConfluentMessage):
def __init__(self, node, media):
self.kvpairs = {node: {'name': media.name, 'url': media.url}}
class InputAlertData(ConfluentMessage):
@@ -372,10 +468,60 @@ class InputAlertData(ConfluentMessage):
return self.alertparams
class InputAttributes(ConfluentMessage):
class InputExpression(ConfluentMessage):
# This is specifically designed to suppress the expansion of an expression
# so that it can make it intact to the pertinent configmanager function
def __init__(self, path, inputdata, nodes=None):
self.nodeattribs = {}
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if nodes is None:
self.attribs = inputdata
return
for node in nodes:
self.nodeattribs[node] = inputdata
def get_attributes(self, node):
if node not in self.nodeattribs:
return {}
nodeattr = deepcopy(self.nodeattribs[node])
return nodeattr
class InputConfigChangeSet(InputExpression):
# For now, this is identical to InputExpression, later it may
# internalize formula expansion, but not now..
def __init__(self, path, inputdata, nodes=None, configmanager=None):
self.cfm = configmanager
super(InputConfigChangeSet, self).__init__(path, inputdata, nodes)
def get_attributes(self, node):
attrs = super(InputConfigChangeSet, self).get_attributes(node)
endattrs = {}
for attr in attrs:
origval = attrs[attr]
if isinstance(origval, str) or isinstance(origval, unicode):
origval = {'expression': origval}
if 'expression' not in origval:
endattrs[attr] = attrs[attr]
else:
endattrs[attr] = list(self.cfm.expand_attrib_expression(
[node], attrs[attr]))[0][1]
return endattrs
class InputAttributes(ConfluentMessage):
# This is particularly designed for attributes, where a simple string
# should become either a string value or a dict with {'expression':} to
# preserve the client provided expression for posterity, rather than
# immediate consumption.
# for things like node configuration or similar, a different class is
# appropriate since it needs to immediately expand an expression.
# with that class, the 'InputExpression' and calling code in attributes.py
# might be deprecated in favor of the generic expression expander
# and a small function in attributes.py to reflect the expansion back
# to the client
def __init__(self, path, inputdata, nodes=None):
self.nodeattribs = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if nodes is None:
@@ -400,21 +546,12 @@ class InputAttributes(ConfluentMessage):
'expression': self.attribs[attrib]}
return
for node in nodes:
if node in inputdata:
nestedmode = True
self.nodeattribs[node] = inputdata[node]
if nestedmode:
for key in inputdata:
if key not in nodes:
raise exc.InvalidArgumentException
else:
for node in nodes:
self.nodeattribs[node] = inputdata
self.nodeattribs[node] = inputdata
def get_attributes(self, node):
if node not in self.nodeattribs:
return {}
nodeattr = self.nodeattribs[node]
nodeattr = deepcopy(self.nodeattribs[node])
for attr in nodeattr:
if type(nodeattr[attr]) in (str, unicode):
try:
@@ -446,19 +583,19 @@ class InputCredential(ConfluentMessage):
def __init__(self, path, inputdata, nodes=None):
self.credentials = {}
nestedmode = False
if not inputdata:
raise exc.InvalidArgumentException('no request data provided')
if len(path) == 4:
inputdata['uid'] = path[-1]
# if the operation is 'create' check if all fields are present
elif ('uid' not in inputdata or 'privilege_level' not in inputdata or
'username' not in inputdata or 'password' not in inputdata):
raise exc.InvalidArgumentException('all fields are required')
if 'uid' not in inputdata:
raise exc.InvalidArgumentException('uid is missing')
missingattrs = []
for attrname in ('uid', 'privilege_level', 'username', 'password'):
if attrname not in inputdata:
missingattrs.append(attrname)
if missingattrs:
raise exc.InvalidArgumentException(
'Required fields missing: {0}'.format(','.join(missingattrs)))
if (isinstance(inputdata['uid'], str) and
not inputdata['uid'].isdigit()):
raise exc.InvalidArgumentException('uid must be a number')
@@ -488,9 +625,9 @@ class InputCredential(ConfluentMessage):
def get_attributes(self, node):
if node not in self.credentials:
return {}
credential = self.credentials[node]
for attr in credentials:
if type(credentials[attr]) in (str, unicode):
credential = deepcopy(self.credentials[node])
for attr in credential:
if type(credential[attr]) in (str, unicode):
try:
# as above, use format() to see if string follows
# expression, store value back in case of escapes
@@ -520,7 +657,7 @@ class ConfluentInputMessage(ConfluentMessage):
if self.keyname not in datum:
raise exc.InvalidArgumentException(
'missing {0} argument'.format(self.keyname))
elif datum[self.keyname] not in self.valid_values:
elif not self.is_valid_key(datum[self.keyname]):
raise exc.InvalidArgumentException(
datum[self.keyname] + ' is not one of ' +
','.join(self.valid_values))
@@ -530,13 +667,15 @@ class ConfluentInputMessage(ConfluentMessage):
if self.keyname not in datum:
raise exc.InvalidArgumentException(
'missing {0} argument'.format(self.keyname))
elif datum[self.keyname] not in self.valid_values:
elif not self.is_valid_key(datum[self.keyname]):
raise exc.InvalidArgumentException(datum[self.keyname] +
' is not one of ' +
','.join(self.valid_values))
for node in nodes:
self.inputbynode[node] = datum[self.keyname]
def is_valid_key(self, key):
return key in self.valid_values
class InputIdentifyMessage(ConfluentInputMessage):
valid_values = set([
@@ -560,6 +699,16 @@ class InputPowerMessage(ConfluentInputMessage):
def powerstate(self, node):
return self.inputbynode[node]
class InputReseatMessage(ConfluentInputMessage):
valid_values = set([
'reseat',
])
keyname = 'reseat'
def is_valid_key(self, key):
return key in self.valid_values or isinstance(key, int)
class InputBMCReset(ConfluentInputMessage):
valid_values = set([
@@ -591,7 +740,7 @@ class InputMCI(ConfluentInputMessage):
class InputNetworkConfiguration(ConfluentInputMessage):
def __init__(self, path, nodes, inputdata):
def __init__(self, path, nodes, inputdata, configmanager=None):
self.inputbynode = {}
self.stripped = False
if not inputdata:
@@ -615,8 +764,27 @@ class InputNetworkConfiguration(ConfluentInputMessage):
if nodes is None:
raise exc.InvalidArgumentException(
'This only supports per-node input')
nodeattrmap = {}
for attr in inputdata:
try:
if inputdata[attr] is not None:
inputdata[attr].format()
except (KeyError, IndexError):
nodeattrmap[attr] = {}
for expanded in configmanager.expand_attrib_expression(
nodes, inputdata[attr]):
node, value = expanded
nodeattrmap[attr][node] = value
if not nodeattrmap:
for node in nodes:
self.inputbynode[node] = inputdata
return
# an expression was encountered
for node in nodes:
self.inputbynode[node] = inputdata
self.inputbynode[node] = deepcopy(inputdata)
for attr in self.inputbynode[node]:
if attr in nodeattrmap:
self.inputbynode[node][attr] = nodeattrmap[attr][node]
def netconfig(self, node):
return self.inputbynode[node]
@@ -689,10 +857,10 @@ class BootDevice(ConfluentChoiceMessage):
valid_paramset = {
'bootmode': valid_bootmodes,
'persistent': set([True, False]),
}
def __init__(self, node, device, bootmode='unspecified'):
def __init__(self, node, device, bootmode='unspecified', persistent=False):
if device not in self.valid_values:
raise Exception("Invalid boot device argument passed in:" +
repr(device))
@@ -703,6 +871,7 @@ class BootDevice(ConfluentChoiceMessage):
node: {
'nextdevice': {'value': device},
'bootmode': {'value': bootmode},
'persistent': {'value': persistent},
}
}
@@ -711,6 +880,7 @@ class InputBootDevice(BootDevice):
def __init__(self, path, nodes, inputdata):
self.bootdevbynode = {}
self.bootmodebynode = {}
self.persistentbynode = {}
if not inputdata:
raise exc.InvalidArgumentException()
if 'nextdevice' not in inputdata:
@@ -732,6 +902,8 @@ class InputBootDevice(BootDevice):
datum['bootmode'] + ' is not one of ' +
','.join(self.valid_bootmodes))
self.bootmodebynode[key] = datum['bootmode']
if 'persistent' in datum:
self.bootmodebynode[key] = datum['persistent']
else:
datum = inputdata
if 'nextdevice' not in datum:
@@ -745,6 +917,8 @@ class InputBootDevice(BootDevice):
self.bootdevbynode[node] = datum['nextdevice']
if 'bootmode' in datum:
self.bootmodebynode[node] = datum['bootmode']
if 'persistent' in datum:
self.persistentbynode[node] = datum['persistent']
def bootdevice(self, node):
return self.bootdevbynode[node]
@@ -752,6 +926,9 @@ class InputBootDevice(BootDevice):
def bootmode(self, node):
return self.bootmodebynode.get(node, 'unspecified')
def persistent(self, node):
return self.persistentbynode.get(node, False)
class IdentifyState(ConfluentChoiceMessage):
valid_values = set([
@@ -762,6 +939,13 @@ class IdentifyState(ConfluentChoiceMessage):
keyname = 'identify'
class ReseatResult(ConfluentChoiceMessage):
valid_values = set([
'success',
])
keyname = 'reseat'
class PowerState(ConfluentChoiceMessage):
valid_values = set([
'on',
@@ -773,6 +957,11 @@ class PowerState(ConfluentChoiceMessage):
])
keyname = 'state'
def __init__(self, node, state, oldstate=None):
super(PowerState, self).__init__(node, state)
if oldstate is not None:
self.kvpairs[node]['oldstate'] = {'value': oldstate}
class BMCReset(ConfluentChoiceMessage):
valid_values = set([
@@ -835,6 +1024,47 @@ class EventCollection(ConfluentMessage):
self.kvpairs = {name: {'events': eventdata}}
class AsyncCompletion(ConfluentMessage):
def __init__(self):
self.stripped = True
self.notnode = True
def raw(self):
return {'_requestdone': True}
class AsyncMessage(ConfluentMessage):
def __init__(self, pair):
self.stripped = True
self.notnode = True
self.msgpair = pair
def raw(self):
rsp = self.msgpair[1]
rspdict = None
if (isinstance(rsp, ConfluentMessage) or
isinstance(rsp, ConfluentNodeError)):
rspdict = rsp.raw()
elif isinstance(rsp, exc.ConfluentException):
rspdict = {'exceptioncode': rsp.apierrorcode,
'exception': rsp.get_error_body()}
elif isinstance(rsp, Exception):
rspdict = {'exceptioncode': 500, 'exception': str(rsp)}
elif isinstance(rsp, dict): # console metadata
rspdict = rsp
else: # terminal text
rspdict = {'data': rsp}
return {'asyncresponse':
{'requestid': self.msgpair[0],
'response': rspdict}}
class AsyncSession(ConfluentMessage):
def __init__(self, id):
self.desc = 'foo'
self.notnode = True
self.stripped = True
self.kvpairs = {'asyncid': id}
class User(ConfluentMessage):
def __init__(self, uid, username, privilege_level, name=None):
self.desc = 'foo'
@@ -1031,7 +1261,7 @@ class Attributes(ConfluentMessage):
self.desc = desc
nkv = {}
self.notnode = name is None
for key in kv.iterkeys():
for key in kv:
if type(kv[key]) in (str, unicode):
nkv[key] = {'value': kv[key]}
else:
@@ -1043,6 +1273,8 @@ class Attributes(ConfluentMessage):
name: nkv
}
class ConfigSet(Attributes):
pass
class ListAttributes(ConfluentMessage):
def __init__(self, name=None, kv=None, desc=''):
+64
View File
@@ -0,0 +1,64 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# A consolidated manage of neighbor table information management.
# Ultimately, this should use AF_NETLINK, but in the interest of time,
# use ip neigh for the moment
import eventlet.green.subprocess as subprocess
import os
neightable = {}
neightime = 0
import re
_validmac = re.compile('..:..:..:..:..:..')
def update_neigh():
global neightable
global neightime
neightable = {}
if os.name == 'nt':
return
ipn = subprocess.Popen(['ip', 'neigh'], stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(neighdata, err) = ipn.communicate()
for entry in neighdata.split('\n'):
entry = entry.split(' ')
if len(entry) < 5 or not entry[4]:
continue
if entry[0] in ('192.168.0.100', '192.168.70.100', '192.168.70.125'):
# Note that these addresses are common static ip addresses
# that are hopelessly ambiguous if there are many
# so ignore such entries and move on
# ideally the system network steers clear of this landmine of
# a subnet, but just in case
continue
if not _validmac.match(entry[4]):
continue
neightable[entry[0]] = entry[4]
neightime = os.times()[4]
def refresh_neigh():
global neightime
if os.name == 'nt':
return
if os.times()[4] > (neightime + 30):
update_neigh()

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