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

Compare commits

..

5 Commits

Author SHA1 Message Date
Jarrod Johnson 99829835e7 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:34:10 -04:00
Jarrod Johnson ad69ce909e 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:37:41 -04:00
Jarrod Johnson a52ca8cea5 Fix nodeeventlog with 'None' entries
Some entries have 'None' fields.  Gracefully tolerate
this scenario.
2016-09-09 09:15:11 -04:00
Jarrod Johnson 73d74bae3e 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 12:51:59 -04:00
Jarrod Johnson c5a0564d41 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 12:51:52 -04:00
35 changed files with 193 additions and 988 deletions
Executable → Regular
View File
+7 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# Copyright 2015-2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
# limitations under the License.
from datetime import datetime as dt
import optparse
import os
import sys
@@ -27,13 +26,13 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange (clear)")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write(
'Usage: {0} <noderange> [clear]\n'.format(
sys.argv[0]))
sys.exit(1)
deletemode = False
@@ -46,7 +45,7 @@ exitcode = 0
def format_event(evt):
retparts = []
if 'timestamp' in evt and evt['timestamp'] is not None:
if 'timestamp' in evt:
display = dt.strptime(evt['timestamp'], '%Y-%m-%dT%H:%M:%S')
retparts.append(display.strftime('%m/%d/%Y %H:%M:%S'))
dscparts = []
+5 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -58,12 +57,12 @@ def printfirm(node, prefix, data):
print('{0}: {1}: {2}'.format(node, prefix, version))
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write(
'Usage: {0} <noderange>\n'.format(
sys.argv[0]))
sys.exit(1)
try:
session = client.Command()
+3 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
# limitations under the License.
import codecs
import optparse
import os
import sys
@@ -29,12 +28,10 @@ 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 = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write('Usage: {0} <noderange>\n'.format(sys.argv[0]))
sys.exit(1)
+3 -6
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# 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,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
@@ -26,12 +25,10 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(usage="Usage: %prog <noderange> [on|off]")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write('Usage: {0} <noderange> [on|off]\n'.format(sys.argv[0]))
sys.exit(1)
identifystate = None
+4 -5
View File
@@ -1,7 +1,7 @@
#!/usr/bin/python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
path = os.path.dirname(os.path.realpath(__file__))
@@ -70,12 +69,12 @@ def printerror(res, node=None):
exitcode = 1
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
(options, args) = argparser.parse_args()
try:
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write(
'Usage: {0} <noderange>\n'.format(
sys.argv[0]))
sys.exit(1)
try:
session = client.Command()
Executable → Regular
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# 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.
@@ -42,7 +42,7 @@ def attrrequested(attr, attrlist, seenattributes):
return True
return False
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange [list of attributes]")
usage="Usage: %prog [options] noderange [list of attributes")
argparser.add_option('-b', '--blame', action='store_true',
help='Show information about how attributes inherited')
(options, args) = argparser.parse_args()
+6 -8
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# 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,7 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import sys
@@ -26,14 +25,13 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange "
"([status|on|off|shutdown|boot|reset])")
(options, args) = argparser.parse_args()
try:
noderange = args[0]
noderange = sys.argv[1]
except IndexError:
argparser.print_help()
sys.stderr.write(
'Usage: {0} <noderange> ([status|on|off|shutdown|boot|reset]\n'.format(
sys.argv[0]))
sys.exit(1)
setstate = None
-86
View File
@@ -1,86 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016-2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import os
import select
import shlex
import subprocess
import sys
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser(
usage="Usage: %prog node commandexpression",
epilog="Expressions are the same as in attributes, e.g. "
"'ipmitool -H {hardwaremanagement.manager}' will be expanded.")
argparser.disable_interspersed_args()
(options, args) = argparser.parse_args()
if len(args) < 2:
argparser.print_help()
sys.exit(1)
c = client.Command()
cmdstr = " ".join(args[1:])
nodeforpopen = {}
popens = []
for exp in c.create('/noderange/{0}/attributes/expression'.format(args[0]),
{'expression': cmdstr}):
ex = exp['databynode']
for node in ex:
cmd = ex[node]['value'].encode('utf-8')
cmdv = shlex.split(cmd)
nopen = subprocess.Popen(
cmdv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
popens.append(nopen)
nodeforpopen[nopen] = node
all = set([])
pipedesc = {}
exitcode = 0
for pop in popens:
node = nodeforpopen[pop]
pipedesc[pop.stdout] = { 'node': node, 'popen': pop, 'type': 'stdout'}
pipedesc[pop.stderr] = {'node': node, 'popen': pop, 'type': 'stderr'}
all.add(pop.stdout)
all.add(pop.stderr)
rdy, _, _ = select.select(all, [], [], 10)
while all and rdy:
for r in rdy:
data = r.readline()
desc = pipedesc[r]
if data:
node = desc['node']
if desc['type'] == 'stdout':
sys.stdout.write('{0}: {1}'.format(node,data))
else:
sys.stderr.write('{0}: {1}'.format(node, data))
else:
pop = desc['popen']
ret = pop.poll()
if ret is not None:
exitcode = exitcode | ret
all.discard(r)
if all:
rdy, _, _ = select.select(all, [], [], 10)
sys.exit(exitcode)
+6 -13
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2017 Lenovo
# Copyright 2015 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
# limitations under the License.
import csv
import datetime
import optparse
import os
import sys
@@ -40,8 +39,8 @@ sensorcollections = {
argparser = optparse.OptionParser(
usage="Usage: %prog [options] noderange ([sensor(s)])")
argparser.add_option('-i', '--interval', type='float',
usage="Usage: %prog [options] noderange [sensor(s)")
argparser.add_option('-i', '--interval', type='int',
help='Interval to do repeated samples over')
argparser.add_option('-n', '--numreadings', type='int',
help='Number of readings to gather')
@@ -60,7 +59,7 @@ if options.numreadings:
try:
noderange = args[0]
except IndexError:
argparser.print_help()
argparser.print_usage()
sys.exit(1)
sensors = []
for sensorgroup in args[1:]:
@@ -147,12 +146,7 @@ def sensorpass(showout=True, appendtime=False):
def format_csv(csvwriter, orderedsensors, resdata, showtime=True):
for nodekey in resdata:
if showtime:
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]
rowdata = [time.strftime('%Y-%m-%dT%H:%M:%S'), nodekey]
else:
rowdata = [nodekey]
for sensorkey in orderedsensors:
@@ -203,8 +197,7 @@ def main():
nextstart = os.times()[4] + options.interval
resdata = sensorpass(linebyline, True)
if options.csv:
format_csv(csvwriter, orderedsensors, resdata,
showtime=options.interval)
format_csv(csvwriter, orderedsensors, resdata)
if options.numreadings:
options.numreadings -= 1
if options.numreadings <= 0:
+6 -4
View File
@@ -26,8 +26,7 @@ if path.startswith('/opt'):
import confluent.client as client
argparser = optparse.OptionParser(
usage='Usage: %prog [options] noderange [default|cd|network|setup|hd]')
argparser = optparse.OptionParser()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
@@ -41,7 +40,9 @@ argparser.add_option('-p', '--persist', dest='persist', action='store_true',
try:
noderange = args[0]
except IndexError:
argparser.print_help()
sys.stderr.write(
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
sys.argv[0]))
sys.exit(1)
bootdev = None
if len(sys.argv) > 2:
@@ -54,6 +55,7 @@ if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
sys.exit(session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
sys.exit(
session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist))
-37
View File
@@ -1,37 +0,0 @@
confetty(1) --- Interactive confluent client
=================================================
## SYNOPSIS
`confetty`
## DESCRIPTION
**confetty** launches an interactive CLI session to the
confluent service. It provides a filesystem-like
view of the confluent interface. It is intended to
be mostly an aid for developing client software, with
day to day administration generally being easier with
the various function specific commands.
## COMMANDS
The CLI may be navigated by shell commands and some other
commands.
* `cd`:
Change the location within the tree
* `ls`:
List the elements within the current directory/tree
* `show` **ELEMENT**, `cat` **ELEMENT**:
Display the result of reading a specific element (by full or relative path)
* `unset` **ELEMENT** **ATTRIBUTE**
For an element with attributes, request to clear the value of the attribue
* `set` **ELEMENT** **ATTRIBUTE**=**VALUE**
Set the specified attribute to the given value
* `start` **ELEMENT**
Start a console session indicated by **ELEMENT** (e.g. /nodes/n1/console/session)
* `rm` **ELEMENT**
Request removal of an element. (e.g. rm events/hardware/log clears log from a node)
-30
View File
@@ -1,30 +0,0 @@
nodeconsole(1) -- Open a console to a confluent node
=====================================================
## SYNOPSIS
`nodeconsole` `node`
## DESCRIPTION
**nodeconsole** opens an interactive console session to a given node. This is the
text or serial console of a system. Exiting is done by hitting `Ctrl-e`, then `c`,
then `.`. Note that console output by default is additionally logged to
`/var/log/confluent/consoles/`**NODENAME**.
## ESCAPE SEQUENCE COMMANDS
While connected to a console, a number of commands may be performed through escape
sequences. To begin an command escape sequence, hit `Ctrl-e`, then `c`. The next
keystroke will be interpreted as a command. The following commands are available.
* `.`:
Exit the session and return to the command prompt
* `b`:
Send a break to the remote console when possible (some console plugins may not support this)
* `o`:
Request confluent to disconnect and reconnect to console. For example if there is suspicion
that the console has gone inoperable, but would work if reconnected.
* `?`:
Get a list of supported commands
* `<enter>`:
Abandon entering an escape sequence command
-48
View File
@@ -1,48 +0,0 @@
nodelist(1) -- List confluent nodes and their attributes
=========================================================
## SYNOPSIS
`nodelist` `noderange`
`nodelist` `noderange` [-b] [<nodeattribute>...]
## DESCRIPTION
**nodelist** queries the confluent server to get information about nodes. In
the simplest form, it simply takes the given noderange(5) and lists the
matching nodes, one line at a time.
If a list of node attribute names are given, the value of those are also
displayed. If `-b` is specified, it will also display information on
how inherited and expression based attributes are defined. There is more
information on node attributes in nodeattributes(5) man page.
## OPTIONS
* `-b`, `--blame`:
Annotate inherited and expression based attributes to show their base value.
## EXAMPLES
* Listing matching nodes of a simple noderange:
`# nodelist n1-n4`
`n1`
`n2`
`n3`
`n4`
* Getting an attribute of nodes matching a noderange:
`# nodelist n1,n2 hardwaremanagement.manager`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n2: hardwaremanagement.manager: 172.30.3.2`
* Getting a group of attributes while determining what group defines them:
`# nodelist n1,n2 hardwaremanegement --blame`
`n1: hardwaremanagement.manager: 172.30.3.1`
`n1: hardwaremanagement.method: ipmi (inherited from group everything)`
`n1: hardwaremanagement.switch: r8e1`
`n1: hardwaremanagement.switchport: 14`
`n2: hardwaremanagement.manager: 172.30.3.2`
`n2: hardwaremanagement.method: ipmi (inherited from group everything)`
`n2: hardwaremanagement.switch: r8e1`
`n2: hardwaremanagement.switchport: 2`
-55
View File
@@ -1,55 +0,0 @@
nodesensors(1) --- Retrieve telemetry for sensors of confluent nodes
====================================================================
## SYNOPSIS
`nodesensors` `noderange` [-c] [-i <interval>] [-n <samplecount>] [<sensor name or category>...]
## DESCRIPTION
**nodesensors** queries the confluent server to get telemetry from nodes. Telemetry can include
data such as temperature, power, and so forth. Without arguments, it lists all available sensors
and their current values. If `-c` is specified, CSV format is used for output. Normally
nodesensors outputs once and exits. Repeated periodic gathering can be done with `-i` to specify
interval and `-n` to specify number of requests. If `-i` is specified without `-n`, then it will
retrieve data at the requested interval indefinitely. If '-n' is specified without `-i`, an
interval of 1 second is used.
## OPTIONS
* `-c`, `--csv`:
Organize output into CSV format, one sensor per column.
* `-i`, `--interval`=**SECONDS**:
Repeat data gathering waiting, waiting the specified time between samples. Unless `-n` is
specified, indefinite retrieval is assumed.
* `-n`, `--numreadings`=**SAMPLES**:
Perform the specified number of readings, waiting `-i` indicated interval or 1 second if not
otherwise indicated.
## EXAMPLES
* Retrieving all temperature related sensors from one system
`# nodesensors n1 temperature`
`n1: CPU 1 Overtemp: Ok`
`n1: CPU 2 Overtemp: Ok`
`n1: Inlet Temp: 16.0 °C`
`n1: PCH Overtemp: Ok`
`n1: LOM Temp: Ok`
* Retrieving a sensor named "Inlet Temp" for 4 systems over a 3 second period of time:
`# nodesensors n1-n4 'Inlet Temp' -c -n 3`
`time,node,Inlet Temp (°C)`
`2016-10-04T15:09:20,n1,19.0`
`2016-10-04T15:09:20,n2,18.0`
`2016-10-04T15:09:20,n3,18.0`
`2016-10-04T15:09:20,n4,17.0`
`2016-10-04T15:09:21,n1,19.0`
`2016-10-04T15:09:21,n2,18.0`
`2016-10-04T15:09:21,n3,18.0`
`2016-10-04T15:09:21,n4,17.0`
`2016-10-04T15:09:22,n1,19.0`
`2016-10-04T15:09:22,n2,18.0`
`2016-10-04T15:09:22,n3,18.0`
`2016-10-04T15:09:22,n4,17.0`
+1 -1
View File
@@ -10,6 +10,6 @@ setup(
scripts=['bin/confetty', 'bin/nodeconsole', 'bin/nodeeventlog',
'bin/nodefirmware', 'bin/nodehealth', 'bin/nodeidentify',
'bin/nodeinventory', 'bin/nodelist', 'bin/nodepower',
'bin/nodesensors', 'bin/nodesetboot', 'bin/noderun'],
'bin/nodesensors', 'bin/nodesetboot'],
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
)
+3 -6
View File
@@ -110,12 +110,9 @@ class AsyncSession(object):
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)
for rsp in handler:
self.add(requestid, rsp)
self.add(requestid, messages.AsyncCompletion())
def get_responses(self, timeout=25):
self.reaper.cancel()
+37 -36
View File
@@ -188,24 +188,12 @@ node = {
# 'appliesto': ['vm'],
# },
'hardwaremanagement.manager': {
'description': 'The management address dedicated to this node. This '
'is the address of, for example, the Lenovo IMM.',
'description': 'The management address dedicated to this node',
},
'hardwaremanagement.method': {
'description': 'The method used to perform operations such as power '
'control, get sensor data, get inventory, and so on. '
},
'hardwaremanagement.switch': {
'description': 'The switch to which the hardware manager is connected.'
' Only relevant if using switch based discovery via the'
' hardware manager (Lenovo IMMs and CMMs). Not '
'applicable to Lenovo Flex nodes.'
},
'hardwaremanagement.switchport': {
'description': 'The port of the switch that the hardware manager is '
'connected. See documentation of '
'hardwaremanagement.switch for more detail.'
},
'enclosure.manager': {
'description': "The management device for this node's chassis",
# 'appliesto': ['system'],
@@ -214,44 +202,40 @@ node = {
'description': 'The bay in the enclosure, if any',
# 'appliesto': ['system'],
},
# 'enclosure.type': {
# 'description': '''The type of enclosure in use (e.g. IBM BladeCenter,
#IBM Flex)''',
# 'appliesto': ['system'],
# },
# 'id.serial': {
# 'inventory.serialnumber': {
# 'description': 'The manufacturer serial number of node',
# },
# 'id.uuid': {
# 'inventory.uuid': {
# 'description': 'The UUID of the node as presented in DMI',
# },
# 'id.modelnumber': {
# 'inventory.modelnumber': {
# 'description': 'The manufacturer dictated model number for the node',
# },
# 'id.modelname': {
# 'description': 'The manufacturer model label for the node',
# },
# 'id.snmpengineid': {
# 'inventory.snmpengineid': {
# 'description': 'The SNMP Engine id used by this node',
# },
# 'secret.snmpuser': {
# 'description': 'The user to use for SNMPv3 access to this node',
# },
# 'secret.snmppassword': {
# 'description': 'The password to use for SNMPv3 access to this node',
# 'secret.snmppassphrase': {
# 'description': 'The passphrase 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.adminpassword': {
# 'secret.snmpcommunity': {
# 'description': ('SNMPv1 community string, it is highly recommended to'
# 'step up to SNMPv3'),
# },
# 'secret.localadminpassphrase': {
# 'description': ('The passphrase to apply to local root/administrator '
# 'account. '
# 'If the environment is 100% Linux, the value may be '
@@ -262,18 +246,35 @@ node = {
# 'AD')
# },
'secret.ipmikg': {
'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)'
'description': 'Optional Integrity key for IPMI communication'
},
# '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': ('The username to use when connecting to the hardware '
'manager'),
'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.'),
},
'secret.hardwaremanagementpassword': {
'description': ('Password to use when connecting to the hardware '
'manager'),
'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'),
},
'pubkeys.addpolicy': {
'description': ('Policy to use when encountering unknown public '
@@ -761,17 +761,7 @@ class ConfigManager(object):
decrypt=self.decrypt)
return nodeobj
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
def get_node_attributes(self, nodelist, attributes=()):
retdict = {}
relattribs = attributes
if isinstance(nodelist, str) or isinstance(nodelist, unicode):
@@ -793,7 +783,7 @@ class ConfigManager(object):
# skipped. The decryption, however, we want to do only on
# demand
nodeobj[attribute] = _decode_attribute(attribute, cfgnodeobj,
decrypt=decrypt)
decrypt=self.decrypt)
retdict[node] = nodeobj
return retdict
@@ -194,8 +194,6 @@ class ConsoleHandler(object):
self._console = plugin.handle_path(
self._plugin_path.format(self.node),
"create", self.cfgmgr)
except exc.NotImplementedException:
self._console = None
except:
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
+4 -16
View File
@@ -78,7 +78,7 @@ def load_plugins():
plugindir = os.path.join(plugintop, plugindir)
if not os.path.isdir(plugindir):
continue
sys.path.insert(1, plugindir)
sys.path.append(plugindir)
# two passes, to avoid adding both py and pyc files
for plugin in os.listdir(plugindir):
if plugin.startswith('.'):
@@ -96,8 +96,6 @@ 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/']
@@ -122,7 +120,6 @@ def _init_core():
'attributes': {
'all': PluginRoute({'handler': 'attributes'}),
'current': PluginRoute({'handler': 'attributes'}),
'expression': PluginRoute({'handler': 'attributes'}),
},
'boot': {
'nextdevice': PluginRoute({
@@ -573,19 +570,10 @@ 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:
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])
return stripnode(passvalues[0], nodes[0])
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
+9 -17
View File
@@ -24,61 +24,53 @@ class ConfluentException(Exception):
apierrorstr = 'Unexpected Error'
def get_error_body(self):
errstr = ' - '.join((self.apierrorstr, str(self)))
return json.dumps({'error': errstr })
return self.apierrorstr
class NotFoundException(ConfluentException):
# Something that could be construed as a name was not found
# basically, picture an http error code 404
apierrorcode = 404
apierrorstr = 'Request path not recognized'
pass
class InvalidArgumentException(ConfluentException):
# Something from the remote client wasn't correct
# like http code 400
apierrorcode = 400
apierrorstr = 'Bad Request'
pass
class TargetEndpointUnreachable(ConfluentException):
# A target system was unavailable. For example, a BMC
# was unreachable. http code 504
apierrorcode = 504
apierrorstr = 'Unreachable Target'
pass
class TargetEndpointBadCredentials(ConfluentException):
# target was reachable, but authentication/authorization
# failed
apierrorcode = 502
apierrorstr = 'Bad Credentials'
pass
class LockedCredentials(ConfluentException):
# A request was performed that required a credential, but the credential
# store is locked
apierrorstr = 'Credential store locked'
pass
class ForbiddenRequest(ConfluentException):
# The client request is not allowed by authorization engine
apierrorcode = 403
apierrorstr = 'Forbidden'
pass
class NotImplementedException(ConfluentException):
# The current configuration/plugin is unable to perform
# the requested task. http code 501
apierrorcode = 501
apierrorstr = '501 - Not Implemented'
pass
class GlobalConfigError(ConfluentException):
# The configuration in the global config file is not right
apierrorstr = 'Global configuration contains an error'
pass
class PubkeyInvalid(ConfluentException):
+41 -106
View File
@@ -35,7 +35,6 @@ import eventlet.greenthread
import greenlet
import json
import socket
import sys
import traceback
import time
import urlparse
@@ -215,44 +214,6 @@ def _should_skip_authlog(env):
return True
return False
def _csrf_valid(env, session):
# This could be simplified into a statement, but this is more readable
# to have it broken out
if (env['REQUEST_METHOD'] == 'GET' and
env['PATH_INFO'] == '/sessions/current/info'):
# Provide a web client a safe hook to request the CSRF token
# This means that we consider GET of /sessions/current/info to be
# a safe thing to inflict via CSRF, since CORS should prevent
# hypothetical attacker from reading the data and it has no
# side effects to speak of
return True
if 'csrftoken' not in session:
# The client has not (yet) requested CSRF protection
# so we return true
if 'HTTP_CONFLUENTAUTHTOKEN' in env:
# The client has requested CSRF countermeasures,
# oblige the request and apply a new token to the
# session
session['csrftoken'] = util.randomstring(32)
elif 'HTTP_REFERER' in env:
# If there is a referrer, make sure it stays consistent
# across the session. A change in referer is a bad thing
try:
referer = env['HTTP_REFERER'].split('/')[2]
except IndexError:
return False
if 'validreferer' not in session:
session['validreferer'] = referer
elif session['validreferer'] != referer:
return False
return True
# The session has CSRF protection enabled, only mark valid if
# the client has provided an auth token and that token matches the
# value protecting the session
return ('HTTP_CONFLUENTAUTHTOKEN' in env and
env['HTTP_CONFLUENTAUTHTOKEN'] == session['csrftoken'])
def _authorize_request(env, operation):
"""Grant/Deny access based on data from wsgi env
@@ -267,30 +228,22 @@ def _authorize_request(env, operation):
cc.load(env['HTTP_COOKIE'])
if 'confluentsessionid' in cc:
sessionid = cc['confluentsessionid'].value
sessid = sessionid
if sessionid in httpsessions:
if _csrf_valid(env, httpsessions[sessionid]):
if env['PATH_INFO'] == '/sessions/current/logout':
targets = []
for mythread in httpsessions[sessionid]['inflight']:
targets.append(mythread)
for mythread in targets:
eventlet.greenthread.kill(mythread)
del httpsessions[sessionid]
return ('logout',)
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if env['PATH_INFO'] == '/sessions/current/logout':
targets = []
for mythread in httpsessions[sessionid]['inflight']:
targets.append(mythread)
for mythread in targets:
eventlet.greenthread.kill(mythread)
del httpsessions[sessionid]
return ('logout',)
httpsessions[sessionid]['expiry'] = time.time() + 90
name = httpsessions[sessionid]['name']
authdata = auth.authorize(
name, element=None,
skipuserobj=httpsessions[sessionid]['skipuserobject'])
if (not authdata) and 'HTTP_AUTHORIZATION' in env:
if env['PATH_INFO'] == '/sessions/current/logout':
if 'HTTP_REFERER' in env:
# note that this doesn't actually do harm
# otherwise, but this way do not give appearance
# of something having a side effect if it has the smell
# of a CSRF
return {'code': 401}
return ('logout',)
name, passphrase = base64.b64decode(
env['HTTP_AUTHORIZATION'].replace('Basic ', '')).split(':', 1)
@@ -303,8 +256,6 @@ def _authorize_request(env, operation):
httpsessions[sessid] = {'name': name, 'expiry': time.time() + 90,
'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
@@ -325,12 +276,10 @@ 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 sessionid is not None:
authinfo['sessionid'] = sessionid
if not skiplog:
auditlog.log(auditmsg)
if 'csrftoken' in httpsessions[sessid]:
authinfo['authtoken'] = httpsessions[sessid]['csrftoken']
return authinfo
else:
return {'code': 401}
@@ -387,13 +336,7 @@ def resourcehandler_backend(env, start_response):
"""Function to handle new wsgi requests
"""
mimetype, extension = _pick_mimetype(env)
headers = [('Content-Type', mimetype), ('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
('X-Content-Type-Options', 'nosniff'),
('Content-Security-Policy', "default-src 'self'"),
('X-XSS-Protection', '1'), ('X-Frame-Options', 'deny'),
('Strict-Transport-Security', 'max-age=86400'),
('X-Permitted-Cross-Domain-Policies', 'none')]
headers = [('Content-Type', mimetype), ('Cache-Control', 'no-cache')]
reqbody = None
reqtype = None
if 'CONTENT_LENGTH' in env and int(env['CONTENT_LENGTH']) > 0:
@@ -406,10 +349,10 @@ def resourcehandler_backend(env, start_response):
del querydict['restexplorerop']
authorized = _authorize_request(env, operation)
if 'logout' in authorized:
start_response('200 Successful logout', headers)
start_response('200 Sucessful logout', headers)
yield('{"result": "200 - Successful logout"}')
return
if 'HTTP_SUPPRESSAUTHHEADER' in env or 'HTTP_CONFLUENTAUTHTOKEN' in env:
if 'HTTP_SUPPRESSAUTHHEADER' in env:
badauth = [('Content-type', 'text/plain')]
else:
badauth = [('Content-type', 'text/plain'),
@@ -518,17 +461,6 @@ def resourcehandler_backend(env, start_response):
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:
@@ -587,10 +519,7 @@ def resourcehandler_backend(env, start_response):
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)
yield json.dumps({'username': authorized['username']})
return
resource = '.' + url[url.rindex('/'):]
lquerydict = copy.deepcopy(querydict)
@@ -612,9 +541,26 @@ 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):
if e.apierrorcode == 500:
# raise generics to trigger the tracelog
raise
start_response('{0} {1}'.format(e.apierrorcode, e.apierrorstr),
@@ -742,20 +688,9 @@ 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
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)
eventlet.wsgi.server(
eventlet.listen((bind_host, bind_port, 0, 0), family=socket.AF_INET6),
resourcehandler, log=False, log_output=False, debug=False)
class HttpApi(object):
+7 -34
View File
@@ -444,17 +444,12 @@ class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
# As time rolling happens, reset statistics count
self.sizeRollingCount = 0
dbfn = self.binpath + "." + time.strftime(self.suffix, timeTuple)
odbfn = dbfn
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
odtfn = dtfn
append=1
while os.path.exists(dbfn):
dbfn = odbfn + '.{}'.format(append)
append += 1
append=1
while os.path.exists(dtfn):
dtfn = odtfn + '.{}'.format(append)
append += 1
if os.path.exists(dbfn):
os.remove(dbfn)
if os.path.exists(dtfn):
os.remove(dtfn)
if os.path.exists(self.binpath):
os.rename(self.binpath, dbfn)
if os.path.exists(self.textpath):
@@ -618,14 +613,8 @@ class Logger(object):
struct.unpack(">BBIHIBBH", recbytes)
# rolling events found.
if ltype == DataTypes.event and evtdata == Events.logrollover:
txtpath, bpath = parse_last_rolling_files(textfile, offset,
datalen)
if txtpath == textpath:
break
if bpath == binpath:
break
textpath = txtpath
binpath = bpath
textpath, binpath = parse_last_rolling_files(textfile, offset,
datalen)
# Rolling event detected, close the current bin file, then open
# the renamed bin file.
flock(binfile, LOCK_UN)
@@ -733,19 +722,3 @@ class Logger(object):
def closelog(self):
self.handler.close()
self.closer = None
globaleventlog = None
tracelog = None
def log(logdata=None, ltype=None, event=0, eventdata=None):
if globaleventlog is None:
globaleventlog = Logger('events')
globaleventlog.log(logdata, ltype, event, eventdata)
def logtrace():
global tracelog
if tracelog is None:
tracelog = Logger('trace')
tracelog.log(traceback.format_exc(), ltype=DataTypes.event,
event=Events.stacktrace)
+6 -14
View File
@@ -1,7 +1,7 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2014 IBM Corporation
# Copyright 2015-2017 Lenovo
# 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.
@@ -95,15 +95,9 @@ def _checkpidfile():
fcntl.flock(pidfile, fcntl.LOCK_EX)
pid = pidfile.read()
if pid != '':
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)
print ('/var/run/confluent/pid exists and indicates %s is still '
'running' % pid)
sys.exit(1)
pidfile.write(str(os.getpid()))
fcntl.flock(pidfile, fcntl.LOCK_UN)
pidfile.close()
@@ -205,12 +199,10 @@ def run():
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
if dbgif:
oumask = os.umask(0077)
try:
os.remove('/var/run/confluent/dbg.sock')
except OSError:
pass # We are not expecting the file to exist
dbgsock = eventlet.listen("/var/run/confluent/dbg.sock",
family=socket.AF_UNIX)
eventlet.spawn_n(backdoor.backdoor_server, dbgsock)
+1 -16
View File
@@ -59,7 +59,6 @@ def _htmlify_structure(indict):
class ConfluentMessage(object):
apicode = 200
readonly = False
defaultvalue = ''
defaulttype = 'text'
@@ -175,15 +174,12 @@ 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: {'errorcode': self.apicode,
'error': self.error}}}
return {'databynode': {self.node: {'error': self.error}}}
def html(self):
return self.node + ":" + self.error
@@ -195,20 +191,15 @@ class ConfluentNodeError(object):
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
@@ -218,7 +209,6 @@ class ConfluentTargetNotFound(ConfluentNodeError):
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
apicode = 502
def __init__(self, node):
self.node = node
self.error = 'bad credentials'
@@ -880,11 +870,6 @@ class AsyncMessage(ConfluentMessage):
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
@@ -1,249 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This provides the implementation of locating MAC addresses on ethernet
# switches. It is, essentially, a port of 'MacMap.pm' to confluent.
# However, there are enhancements.
# For one, each switch interrogation is handled in an eventlet 'thread'
# For another, MAC addresses are checked in the dictionary on every
# switch return, rather than waiting for all switches to check in
# (which makes it more responsive when there is a missing or bad switch)
# Also, we track the quantity, actual ifName value, and provide a mechanism
# to detect ambiguous result (e.g. if two matches are found, can log an error
# rather than doing the wrong one, complete with the detected ifName value).
# Further, the map shall be available to all facets of the codebase, not just
# the discovery process, so that the cached data maintenance will pay off
# for direct queries
# this module will provide mac to switch and full 'ifName' label
# This functionality is restricted to the null tenant
import confluent.exceptions as exc
import confluent.log as log
import confluent.snmputil as snmp
from eventlet.greenpool import GreenPool
import re
_macmap = {}
_macsbyswitch = {}
_nodesbymac = {}
_switchportmap = {}
_whitelistnames = (
# 3com
re.compile(r'^RMON Port (\d+) on unit \d+'),
# Dell
re.compile(r'^Unit \d+ Port (\d+)\Z'),
)
_blacklistnames = (
re.compile(r'vl'),
re.compile(r'Nu'),
re.compile(r'RMON'),
re.compile(r'onsole'),
re.compile(r'Stack'),
re.compile(r'Trunk'),
re.compile(r'po\d'),
re.compile(r'XGE'),
re.compile(r'LAG'),
re.compile(r'CPU'),
re.compile(r'Management'),
)
def _namesmatch(switchdesc, userdesc):
if switchdesc == userdesc:
return True
try:
portnum = int(userdesc)
except ValueError:
portnum = None
if portnum is not None:
for exp in _whitelistnames:
match = exp.match(switchdesc)
if match:
snum = int(match.groups()[0])
if snum == portnum:
return True
anymatch = re.search(r'[^0123456789]' + userdesc + r'(\.0)?\Z', switchdesc)
if anymatch:
for blexp in _blacklistnames:
if blexp.match(switchdesc):
return False
return True
return False
def _map_switch(args):
try:
return _map_switch_backend(args)
except Exception as e:
log.logtrace()
def _nodelookup(switch, ifname):
"""Get a nodename for a given switch and interface name
"""
for portdesc in _switchportmap.get(switch, {}):
if _namesmatch(ifname, portdesc):
return _switchportmap[switch][portdesc]
return None
def _map_switch_backend(args):
"""Manipulate portions of mac address map relevant to a given switch
"""
# 1.3.6.1.2.1.17.7.1.2.2.1.2 - mactoindex (qbridge - preferred)
# if not, check for cisco and if cisco, build list of all relevant vlans:
# .1.3.6.1.4.1.9.9.46.1.6.1.1.5 - trunk port vlan map (cisco only)
# .1.3.6.1.4.1.9.9.68.1.2.2.1.2 - access port vlan map (cisco only)
# if cisco, vlan community string indexed or snmpv3 contest for:
# 1.3.6.1.2.1.17.4.3.1.2 - mactoindx (bridge - low-end switches and cisco)
# .1.3.6.1.2.1.17.1.4.1.2 - bridge index to if index map
# no vlan index or context for:
# .1.3.6.1.2.1.31.1.1.1.1 - ifName... but some switches don't do it
# .1.3.6.1.2.1.2.2.1.2 - ifDescr, usually useless, but a
# fallback if ifName is empty
#
global _macmap
switch, password, user = args
haveqbridge = False
mactobridge = {}
conn = snmp.Session(switch, password, user)
for vb in conn.walk('1.3.6.1.2.1.17.7.1.2.2.1.2'):
haveqbridge = True
oid, bridgeport = vb
if not bridgeport:
continue
oid = str(oid).rsplit('.', 6) # if 7, then oid[1] would be vlan id
macaddr = '{0:02x}:{1:02x}:{2:02x}:{3:02x}:{4:02x}:{5:02x}'.format(
*([int(x) for x in oid[-6:]])
)
mactobridge[macaddr] = int(bridgeport)
if not haveqbridge:
raise exc.NotImplementedException('TODO: Bridge-MIB without QBRIDGE')
bridgetoifmap = {}
for vb in conn.walk('1.3.6.1.2.1.17.1.4.1.2'):
bridgeport, ifidx = vb
bridgeport = int(str(bridgeport).rsplit('.', 1)[1])
bridgetoifmap[bridgeport] = int(ifidx)
ifnamemap = {}
havenames = False
for vb in conn.walk('1.3.6.1.2.1.31.1.1.1.1'):
ifidx, ifname = vb
if not ifname:
continue
havenames = True
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
if not havenames:
for vb in conn.walk( '1.3.6.1.2.1.2.2.1.2'):
ifidx, ifname = vb
ifidx = int(str(ifidx).rsplit('.', 1)[1])
ifnamemap[ifidx] = str(ifname)
maccounts = {}
for mac in mactobridge:
ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]]
if ifname not in maccounts:
maccounts[ifname] = 1
else:
maccounts[ifname] += 1
_macsbyswitch[switch] = {}
for mac in mactobridge:
# We want to merge it so that when a mac appears in multiple
# places, it is captured.
ifname = ifnamemap[bridgetoifmap[mactobridge[mac]]]
if mac in _macmap:
_macmap[mac].append((switch, ifname, maccounts[ifname]))
else:
_macmap[mac] = [(switch, ifname, maccounts[ifname])]
if ifname in _macsbyswitch[switch]:
_macsbyswitch[switch][ifname].append(mac)
else:
_macsbyswitch[switch][ifname] = [mac]
nodename = _nodelookup(switch, ifname)
if nodename is not None:
if mac in _nodesbymac and _nodesbymac[mac] != nodename:
log.log({'warning': '{0} and {1} described by ambiguous'
' switch topology values'.format(nodename,
_nodesbymac[mac]
)})
_nodesbymac[mac] = nodename
def update_macmap(configmanager):
"""Interrogate switches to build/update mac table
Begin a rebuild process. This process is a generator that will yield
as each switch interrogation completes, allowing a caller to
recheck the cache as results become possible, rather
than having to wait for the process to complete to interrogate.
"""
global _macmap
global _nodesbymac
global _switchportmap
# Clear all existing entries
_macmap = {}
_nodesbymac = {}
_switchportmap = {}
if configmanager.tenant is not None:
raise exc.ForbiddenRequest('Network topology not available to tenants')
nodelocations = configmanager.get_node_attributes(
configmanager.list_nodes(), ('hardwaremanagement.switch',
'hardwaremanagement.switchport'))
switches = set([])
for node in nodelocations:
cfg = nodelocations[node]
if 'hardwaremanagement.switch' in cfg:
curswitch = cfg['hardwaremanagement.switch']['value']
switches.add(curswitch)
if 'hardwaremanagement.switchport' in cfg:
portname = cfg['hardwaremanagement.switchport']['value']
if curswitch not in _switchportmap:
_switchportmap[curswitch] = {}
if portname in _switchportmap[curswitch]:
log.log({'warning': 'Duplicate switch topology config for '
'{0} and {1}'.format(node,
_switchportmap[
curswitch][
portname])})
_switchportmap[curswitch][portname] = node
switchcfg = configmanager.get_node_attributes(
switches, ('secret.hardwaremanagementuser',
'secret.hardwaremanagementpassword'), decrypt=True)
switchauth = []
for switch in switches:
password = 'public'
user = None
if (switch in switchcfg and
'secret.hardwaremanagementpassword' in switchcfg[switch]):
password = switchcfg[switch]['secret.hardwaremanagementpassword'][
'value']
if 'secret.hardwaremanagementuser' in switchcfg[switch]:
user = switchcfg[switch]['secret.hardwaremanagementuser'][
'value']
switchauth.append((switch, password, user))
pool = GreenPool()
for res in pool.imap(_map_switch, switchauth):
yield res
print(repr(_macmap))
if __name__ == '__main__':
# invoke as switch community
import sys
_map_switch(sys.argv[1], sys.argv[2])
@@ -152,20 +152,6 @@ def update_nodegroup(group, element, configmanager, inputdata):
return retrieve_nodegroup(group, element, configmanager, inputdata)
def _expand_expression(nodes, configmanager, inputdata):
expression = inputdata.get_attributes(list(nodes)[0])
if type(expression) is dict:
expression = expression['expression']
if type(expression) is dict:
expression = expression['expression']
for expanded in configmanager.expand_attrib_expression(nodes, expression):
yield msg.KeyValueData({'value': expanded[1]}, expanded[0])
def create(nodes, element, configmanager, inputdata):
if nodes is not None and element[-1] == 'expression':
return _expand_expression(nodes, configmanager, inputdata)
def update_nodes(nodes, element, configmanager, inputdata):
updatedict = {}
for node in nodes:
@@ -51,7 +51,6 @@ sensor_categories = {
'fans': frozenset(['Fan', 'Cooling Device']),
}
def hex2bin(hexstring):
hexvals = hexstring.split(':')
if len(hexvals) < 2:
@@ -61,7 +60,6 @@ def hex2bin(hexstring):
bytedata = [int(i, 16) for i in hexvals]
return bytearray(bytedata)
def simplify_name(name):
return name.lower().replace(' ', '_').replace('/', '-')
@@ -344,7 +342,6 @@ class IpmiHandler(object):
except socket.gaierror as ge:
if ge[0] == -2:
raise exc.TargetEndpointUnreachable(ge[1])
raise
self.ipmicmd = persistent_ipmicmds[(node, tenant)]
bootdevices = {
@@ -356,9 +353,8 @@ class IpmiHandler(object):
self.broken = True
self.error = response['error']
else:
self.ipmicmd = ipmicmd
self.loggedin = True
self.ipmicmd.setup_confluent_keyhandler()
ipmicmd.setup_confluent_keyhandler()
self._logevt.set()
def handle_request(self):
@@ -855,12 +851,7 @@ class IpmiHandler(object):
else:
idx = int(self.element[-1]) - 1
servers = self.ipmicmd.get_ntp_servers()
if len(servers) > idx:
self.output.put(msg.NTPServer(self.node, servers[idx]))
else:
self.output.put(
msg.ConfluentTargetNotFound(
self.node, 'Requested NTP configuration not found'))
self.output.put(msg.NTPServer(self.node, servers[idx]))
return
elif self.op in ('update', 'create'):
if self.element[-1] == 'all':
@@ -117,15 +117,6 @@ class SshShell(conapi.Console):
def write(self, data):
if self.inputmode == 0:
while len(data) and data[0] == b'\x7f' and len(self.username):
self.datacallback('\b \b') # erase previously echoed value
self.username = self.username[:-1]
data = data[1:]
while len(data) and data[0] == b'\x7f':
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.username += data
if '\r' in self.username:
self.username, self.password = self.username.split('\r')
@@ -134,16 +125,10 @@ class SshShell(conapi.Console):
self.datacallback(lastdata)
self.datacallback('\r\nEnter password: ')
self.inputmode = 1
elif len(data) > 0:
else:
# echo back typed data
self.datacallback(data)
elif self.inputmode == 1:
while len(data) > 0 and data[0] == b'\x7f':
self.password = self.password[:-1]
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.password += data
if '\r' in self.password:
self.password = self.password.split('\r')[0]
-103
View File
@@ -1,103 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2016 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This provides a simplified wrapper around snmp implementation roughly
# mapping to the net-snmp commands
# net-snmp-python was considered as the API is cleaner, but the ability to
# patch pysnmp to have it be eventlet friendly has caused it's selection
# This module simplifies the complex hlapi pysnmp interface
import confluent.exceptions as exc
import eventlet
from eventlet.support.greendns import getaddrinfo
import socket
snmp = eventlet.import_patched('pysnmp.hlapi')
def _get_transport(name):
# Annoyingly, pysnmp does not automatically determine ipv6 v ipv4
res = getaddrinfo(name, 161, 0, socket.SOCK_DGRAM)
if res[0][0] == socket.AF_INET6:
return snmp.Udp6TransportTarget(res[0][4])
else:
return snmp.UdpTransportTarget(res[0][4])
class Session(object):
def __init__(self, server, secret, username=None, context=None):
"""Create a new session to interrogate a switch
If username is not given, it is assumed that
the secret is community string, and v2c is used. If a username given,
it'll assume SHA auth and DES privacy with the secret being the same
for both.
:param server: The network name/address to target
:param secret: The community string or password
:param username: The username for SNMPv3
:param context: The SNMPv3 context or index for community indexing
"""
self.server = server
self.context = context
if username is None:
# SNMP v2c
self.authdata = snmp.CommunityData(secret, mpModel=1)
else:
self.authdata = snmp.UsmUserData(username, authKey=secret,
privKey=secret)
self.eng = snmp.SnmpEngine()
def walk(self, oid):
"""Walk over children of a given OID
This is roughly equivalent to snmpwalk. It will automatically try to
be a snmpbulkwalk if possible.
:param oid: The SNMP object identifier
"""
# SNMP is a complicated mess of things. Will endeavor to shield caller
# from as much as possible, assuming reasonable defaults when possible.
# there may come a time where we add more parameters to override the
# automatic behavior (e.g. DES is weak, so it's likely to be
# overriden, but some devices only support DES)
tp = _get_transport(self.server)
ctx = snmp.ContextData(self.context)
if '::' in oid:
mib, field = oid.split('::')
obj = snmp.ObjectType(snmp.ObjectIdentity(mib, field))
else:
obj = snmp.ObjectType(snmp.ObjectIdentity(oid))
walking = snmp.bulkCmd(self.eng, self.authdata, tp, ctx, 0, 10, obj,
lexicographicMode=False)
for rsp in walking:
errstr, errnum, erridx, answers = rsp
if errstr:
raise exc.TargetEndpointUnreachable(str(errstr))
elif errnum:
raise exc.ConfluentException(errnum.prettyPrint())
for ans in answers:
yield ans
if __name__ == '__main__':
import sys
ts = Session(sys.argv[1], 'public')
for kp in ts.walk(sys.argv[2]):
print(str(kp[0]))
print(str(kp[1]))
+32 -13
View File
@@ -118,9 +118,37 @@ def sessionhdl(connection, authname, skipauth=False):
try:
process_request(
connection, request, cfm, authdata, authname, skipauth)
except exc.ForbiddenRequest:
send_data(connection, {'errorcode': 403,
'error': 'Forbidden'})
send_data(connection, {'_requestdone': 1})
except exc.TargetEndpointBadCredentials:
send_data(connection, {'errorcode': 502,
'error': 'Bad Credentials'})
send_data(connection, {'_requestdone': 1})
except exc.TargetEndpointUnreachable as tu:
send_data(connection, {'errorcode': 504,
'error': 'Unreachable Target - ' + str(
tu)})
send_data(connection, {'_requestdone': 1})
except exc.NotImplementedException:
send_data(connection, {'errorcode': 501,
'error': 'Not Implemented'})
send_data(connection, {'_requestdone': 1})
except exc.NotFoundException as nfe:
send_data(connection, {'errorcode': 404,
'error': str(nfe)})
send_data(connection, {'_requestdone': 1})
except exc.InvalidArgumentException as iae:
send_data(connection, {'errorcode': 400,
'error': 'Bad Request - ' + str(iae)})
send_data(connection, {'_requestdone': 1})
except exc.LockedCredentials as lockedcred:
send_data(connection, {'errorcode': 500,
'error': 'Locked Credential Store'})
send_data(connection, {'_requestdone': 1})
except exc.ConfluentException as e:
if ((not isinstance(e, exc.LockedCredentials)) and
e.apierrorcode == 500):
if e.apierrorcode == 500:
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
event=log.Events.stacktrace)
send_data(connection, {'errorcode': e.apierrorcode,
@@ -148,7 +176,7 @@ def send_response(responses, connection):
def process_request(connection, request, cfm, authdata, authname, skipauth):
if not isinstance(request, dict):
raise exc.InvalidArgumentException
raise ValueError
operation = request['operation']
path = request['path']
params = request.get('parameters', {})
@@ -244,16 +272,7 @@ def _tlshandler(bind_host, bind_port):
plainsocket = socket.socket(socket.AF_INET6)
plainsocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
plainsocket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
bound = False
while not bound:
try:
plainsocket.bind((bind_host, bind_port, 0, 0))
bound = True
except socket.error as e:
if e.errno != 98:
raise
sys.stderr.write('TLS Socket in use, retrying in 1 second\n')
eventlet.sleep(1)
plainsocket.bind((bind_host, bind_port, 0, 0))
plainsocket.listen(5)
while (1): # TODO: exithook
cnn, addr = plainsocket.accept()
+1 -4
View File
@@ -12,7 +12,7 @@ Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Requires: python-pyghmi, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dns
Requires: python-pyghmi, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dnspython
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
Url: http://xcat.sf.net/
@@ -33,9 +33,6 @@ done
grep -v confluent/__init__.py INSTALLED_FILES.bare > INSTALLED_FILES
cat INSTALLED_FILES
%post
if [ -x /usr/bin/systemctl ]; then /usr/bin/systemctl try-restart confluent; fi
%clean
rm -rf $RPM_BUILD_ROOT
+4 -4
View File
@@ -1,8 +1,8 @@
cd `dirname $0`
VERSION=`git describe|cut -d- -f 1`
NUMCOMMITS=`git describe|cut -d- -f 2`
if [ "$NUMCOMMITS" != "$VERSION" ]; then
VERSION=$VERSION.dev$NUMCOMMITS.g`git describe|cut -d- -f 3`
VERSION=`git tag -l|tail -n 1`
NUMCOMMITS=`git rev-list $VERSION..HEAD|wc -l`
if [ "$NUMCOMMITS" -ne 0 ]; then
VERSION=$VERSION.dev$NUMCOMMITS.g`git rev-parse --short HEAD`
fi
echo $VERSION > VERSION
sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py