mirror of
https://github.com/xcat2/confluent.git
synced 2026-09-29 08:41:00 +00:00
Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bc5efa8a7e | |||
| 486c322233 | |||
| 548e4404ce | |||
| 99e97fe5c4 | |||
| 8c13e738c0 | |||
| 5ffc2c298b | |||
| 7bf8242aba | |||
| b3c28ad33e | |||
| e7bdb5ee7d | |||
| 8eef064b9f | |||
| cb8cd29022 | |||
| c016c55340 | |||
| d2156f3d67 | |||
| bf8dff90f3 | |||
| 004d40e7ca | |||
| ac084b212e | |||
| 7557136d5d | |||
| 70762d3f6c | |||
| eeb668bdfa | |||
| 61bd415ec4 | |||
| 107337fdba | |||
| eb02247a58 | |||
| 53904a2a5a | |||
| 5881ad8b68 | |||
| e0211fd8d8 | |||
| 0ad4ae90c9 | |||
| 6f9bdf4a7c | |||
| 4eaab9db37 | |||
| 92fa2bf4d9 | |||
| 7f9394b33a | |||
| 05a6664165 | |||
| 457f1fe30b | |||
| 2efadf21b5 | |||
| bb38ff4588 | |||
| d4ffc41451 | |||
| fbd5059ade | |||
| baf8587759 | |||
| 7eb881d7e5 | |||
| 9f0daf324e | |||
| ddbc155d6b | |||
| 9d86ffee92 | |||
| 1286f8af3c | |||
| d183ef768d | |||
| d19fdad0ba | |||
| b503d9ca11 | |||
| 9e4ee7bb31 | |||
| 4d04c1fb18 | |||
| 1085e342fd | |||
| 05e642ada5 | |||
| 00da61b981 | |||
| 786a1ec93e | |||
| 27524ab3ce | |||
| 7b160bd99c | |||
| 9516efd74a | |||
| 5410b394f2 | |||
| 801a4c4b1e | |||
| 29da853bcf | |||
| 7a72de6033 | |||
| b9733b3e0e | |||
| 4aeb7e1df5 | |||
| 147b3952e0 | |||
| 54e135f210 | |||
| 958be7d004 | |||
| 7a4c9a1fc0 | |||
| 9764a02419 | |||
| f539a4e4b6 | |||
| 6b5f437a1c | |||
| 8387f0e13e | |||
| ee679b745e | |||
| 3c876566a6 | |||
| f85ee82df3 | |||
| 20abffdbee | |||
| 2dd44b1725 | |||
| f4e8dd497f | |||
| 9a93baed0e | |||
| 41e84c7c47 | |||
| a046e4939f | |||
| e4aa873141 | |||
| ec02097b52 | |||
| 5d105c43e5 | |||
| ca91cfb220 | |||
| b328c53d91 | |||
| 129f034c07 | |||
| b5fbfe730d | |||
| d9e47824a4 | |||
| 96670784f9 | |||
| 16c7429900 | |||
| 14f6fabe0a | |||
| d5e833480e | |||
| e949ee932a | |||
| b524af08b3 | |||
| df74753908 | |||
| bb0e256a98 | |||
| 26da687dc3 | |||
| fa3a402708 | |||
| 0672666e42 | |||
| d4357c6984 | |||
| 4ba8a7a997 | |||
| fa0c0ce81a | |||
| d3bda4217c | |||
| 22509946c0 | |||
| f8b878b5f4 | |||
| 91a1c0ef7d | |||
| 419fcf1577 | |||
| 06e767e70e | |||
| 94d2be4a87 | |||
| 2ea7ee0dcb | |||
| 417e70e5c1 | |||
| 03b2cdab5a | |||
| 79b1268a75 | |||
| 50aefee728 | |||
| 44a5c2b464 | |||
| 2dd6c31513 | |||
| d753ac2833 | |||
| 3cd96a4f59 | |||
| 2b3d5f7b62 | |||
| 75a747a6a2 | |||
| 8fac1ce5da | |||
| 7d67ea0685 | |||
| bcb9c2660f | |||
| 6504acecad | |||
| d1247cfb37 | |||
| c5e19fe474 | |||
| 58bf72d5aa | |||
| f15cf014e9 | |||
| fb1e20906e | |||
| 1bf124494e | |||
| 9d40c67974 | |||
| f75f2cae51 | |||
| 5ae0f37f97 | |||
| 7ff20e3e39 | |||
| 0e42e83c50 | |||
| 378df2966f | |||
| b6546f923b | |||
| 40007a6a07 | |||
| b98889b54a | |||
| 8bf7a55b68 | |||
| e9f2d7eb63 | |||
| 5ab6a9e7b7 | |||
| 64751bccee | |||
| 244f655055 | |||
| 1b26b2cf3d | |||
| 029c06cc66 | |||
| 1df60ceb73 | |||
| 875cda00ff | |||
| f20cdfe49a | |||
| ba6b7cf517 | |||
| 76ff9fd759 | |||
| 18280ccd8a | |||
| 37f1acae1d | |||
| 44103b31f8 | |||
| 774d592eb4 | |||
| 824253ae8c | |||
| a574c69535 | |||
| a2445e7f65 | |||
| 0b51edde97 | |||
| 6b014deb04 | |||
| 366de1235c | |||
| 1d67f10432 | |||
| 6e1adc88dd | |||
| 2419d95b74 | |||
| 62801734ab | |||
| cf16bfdd95 | |||
| d0bd275cb3 | |||
| a332678312 | |||
| cfafa5a5bc | |||
| 23f025eb71 | |||
| 5695bf5288 | |||
| 3d926bb264 | |||
| c8e5644061 | |||
| ff857bce14 | |||
| 9146fce016 | |||
| afa67f9bf7 | |||
| d54e31a1c5 | |||
| 15ddb554f9 | |||
| 4a660d2fb1 | |||
| 849193cf98 | |||
| 00feca7e5b | |||
| be75018609 | |||
| a891745386 | |||
| 46d3779774 | |||
| 5d73548583 | |||
| 8ae8b79837 | |||
| 0df21ddeb0 | |||
| 0e821a7bfe | |||
| 30ed563810 | |||
| 89edc020d5 | |||
| e2b79a063d | |||
| f6ce9f2c1e | |||
| ba9d62b4e5 | |||
| 4442ce1c71 | |||
| 1d64792cb9 | |||
| e721f8836e | |||
| 6770fe9342 | |||
| 804d4c2d95 | |||
| f946fab3c5 | |||
| 5a0ac899b9 | |||
| dfc1e32546 | |||
| 310bd11669 | |||
| 5ab1d6ea59 | |||
| d03ca6eafe | |||
| 4d148751e5 | |||
| d8c3b2f267 | |||
| f02c74cce0 | |||
| 36dd493021 | |||
| a3e5388303 | |||
| 5485ef36bf | |||
| 1cf238708f | |||
| 2ff97b93ef | |||
| 80aa2c477a | |||
| 9868e55958 | |||
| 7a8fbe7d62 | |||
| 52aaeef506 | |||
| be8d82c6c1 | |||
| a83583a56d | |||
| f906cebca3 | |||
| e153e8acd0 | |||
| dec0543ce3 | |||
| 204f6de7e9 | |||
| 76906c191b | |||
| 61a0a66486 | |||
| 597bc9413f | |||
| e53a9f83f4 | |||
| 31a191cb10 | |||
| 8d566c1795 | |||
| 2c2884f80f | |||
| fe4797857d | |||
| e90204cc4d | |||
| d4828b2115 | |||
| 7b26d2edfb | |||
| 0bb0368d07 | |||
| 1467e1f172 | |||
| 06f22e7acb | |||
| 8779f1c27f | |||
| 2245e53ff1 | |||
| 99d1edcdef | |||
| fbca02e262 | |||
| d533321d55 | |||
| 0b7b713f3f | |||
| 865545b8d1 | |||
| 258e257939 | |||
| 127fc59195 | |||
| 8c4d33db92 | |||
| b1240e327f | |||
| 477418a56e | |||
| 9b52e276cc | |||
| fa545eeaee | |||
| 5590b4138e | |||
| 098c78558b | |||
| 57a3c6d287 | |||
| 3790984555 | |||
| b88a135602 | |||
| 2115fb3f78 | |||
| 1f8bc635a8 | |||
| f78a4d6074 | |||
| d9ed98d58e | |||
| db3320e2ec | |||
| a02f96710b | |||
| 03de545e09 | |||
| fcd0e523a0 | |||
| 0ae2b7acc6 | |||
| c50be7c3f2 | |||
| 45f62b5c05 | |||
| a0e6e0b5c6 | |||
| 9975d57d5e | |||
| c1bd46b22a | |||
| ff213916b6 | |||
| 5128a80c79 | |||
| dc436fda74 | |||
| ad20193309 | |||
| 0bd9b08be2 | |||
| e9a31f52ae | |||
| a3dcf88749 | |||
| 765c15ed5b | |||
| 34960bff22 | |||
| 29417d935c | |||
| b48cd8b685 | |||
| a1a61dfdbd | |||
| 11d4628458 | |||
| 4dcfaf7363 | |||
| 655a0b4d17 | |||
| 668f1c98b6 | |||
| b1b4ee4634 | |||
| e207940e50 | |||
| e02dc74eb7 | |||
| 8114c3dc6a | |||
| fde68e1320 | |||
| 32184d463d | |||
| e7ff4fdd93 | |||
| 044def59ba | |||
| 419c41e2cc | |||
| c20f0dc2ae | |||
| 455feb867d | |||
| dc0c7270a4 | |||
| ceee6173da | |||
| 2fb63a34ba | |||
| 55c333b198 | |||
| 56455fe497 | |||
| 87552b9f03 | |||
| 6ba7072aed | |||
| f7b383b692 | |||
| a0e3dca856 | |||
| 90ad5829fc | |||
| 669661b530 | |||
| e17b9e98a8 | |||
| aef26abd56 | |||
| 57bacd69c1 | |||
| 88aa696d29 | |||
| 548b71cd6c | |||
| c263c2eebb | |||
| 7a084bf538 | |||
| cbf595df26 | |||
| 680f60114e | |||
| ff52ad4740 | |||
| 9626491c41 | |||
| 3f1d49c30b | |||
| 5f1ddc7f84 | |||
| 6e5a7e15d9 | |||
| 5388f497d4 | |||
| cff997bd0b | |||
| e55f55677b | |||
| 905c41b021 | |||
| 19dd0539a9 | |||
| 4d1d016325 | |||
| aa4783672d | |||
| 859aad793d | |||
| f52eec6c19 | |||
| 24eb872090 | |||
| d27df8fffc | |||
| 4aef8524e9 | |||
| 14a9220acb | |||
| 49bff93eed | |||
| 97c928350c | |||
| 2d9df67272 | |||
| e11a749fa4 | |||
| 186929d217 | |||
| 86f66e9795 | |||
| 5a610f23ca | |||
| 3d5fa74f4f | |||
| fefe1dc9e8 | |||
| 7b02954da8 | |||
| 6ab1ae0c38 | |||
| 6a1da9e84f | |||
| c6d0d87ca9 | |||
| bfc7ea2b51 | |||
| 7baec5a69f | |||
| 7cda6f7d6e | |||
| 2ab2fbda27 | |||
| 6204628f43 | |||
| b97cd79c3a | |||
| d2f400d982 | |||
| 46b7550a41 | |||
| a28b67e9aa | |||
| 4090dac50c | |||
| c98d2e32d3 | |||
| 2fe0425191 | |||
| 27437048a6 | |||
| 17b5d5a816 | |||
| 26a969c41a | |||
| 093e9faec4 |
@@ -0,0 +1 @@
|
||||
include confluent_env.sh
|
||||
@@ -1 +0,0 @@
|
||||
1.1
|
||||
@@ -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.
|
||||
@@ -41,19 +41,21 @@
|
||||
# esc-( would interfere with normal esc use too much
|
||||
# ~ I will not use for now...
|
||||
|
||||
import fcntl
|
||||
import math
|
||||
import getpass
|
||||
import optparse
|
||||
import os
|
||||
import readline
|
||||
import select
|
||||
import shlex
|
||||
import socket
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
try:
|
||||
import fcntl
|
||||
import termios
|
||||
import tty
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
exitcode = 0
|
||||
consoleonly = False
|
||||
@@ -73,8 +75,11 @@ conserversequence = '\x05c' # ctrl-e, c
|
||||
|
||||
oldtcattr = None
|
||||
fd = sys.stdin
|
||||
if fd.isatty():
|
||||
oldtcattr = termios.tcgetattr(fd.fileno())
|
||||
try:
|
||||
if fd.isatty():
|
||||
oldtcattr = termios.tcgetattr(fd.fileno())
|
||||
except NameError:
|
||||
pass
|
||||
netserver = None
|
||||
laststate = {}
|
||||
|
||||
@@ -82,11 +87,13 @@ laststate = {}
|
||||
def updatestatus(stateinfo={}):
|
||||
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
|
||||
@@ -248,6 +255,9 @@ def print_result(res):
|
||||
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])
|
||||
continue
|
||||
elif 'value' in res[key] and res[key]['value'] is not None:
|
||||
attrstr = '%s="%s"' % (key, res[key]['value'])
|
||||
elif 'value' in res[key] and res[key]['value'] is None:
|
||||
@@ -258,7 +268,10 @@ def print_result(res):
|
||||
attrstr = '%s=""' % key
|
||||
else:
|
||||
sys.stdout.write('{0}: '.format(key))
|
||||
print_result(res[key])
|
||||
if isinstance(res[key], str) or isinstance(res[key], unicode):
|
||||
print res[key]
|
||||
else:
|
||||
print_result(res[key])
|
||||
continue
|
||||
if res[key] is not None and 'inheritedfrom' in res[key]:
|
||||
notes.append(
|
||||
@@ -296,12 +309,38 @@ def do_command(command, server):
|
||||
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])
|
||||
@@ -355,9 +394,11 @@ def do_command(command, server):
|
||||
else:
|
||||
sys.stderr.write("%s: command not found...\n" % argv[0])
|
||||
|
||||
|
||||
def shutdown():
|
||||
tlvdata.send(session.connection, {'operation': 'shutdown', 'path': '/'})
|
||||
|
||||
|
||||
def createresource(args):
|
||||
resname = args[0]
|
||||
attribs = args[1:]
|
||||
@@ -489,6 +530,18 @@ def quitconfetty(code=0, fullexit=False, fixterm=True):
|
||||
inconsole = False
|
||||
|
||||
|
||||
def get_session_node(shellargs):
|
||||
# straight to node console
|
||||
if len(shellargs) == 1 and ' ' not in shellargs[0]:
|
||||
return shellargs[0]
|
||||
if len(shellargs) == 2 and shellargs[0] == 'start':
|
||||
args = [s for s in shellargs[1].split('/') if s]
|
||||
if len(args) == 4 and args[0] == 'nodes' and args[2] == 'console' and \
|
||||
args[3] == 'session':
|
||||
return args[1]
|
||||
return None
|
||||
|
||||
|
||||
def conserver_command(filehandle, command):
|
||||
# x - conserver has that as 'show baud', I am inclined to replace that with
|
||||
# 'request exclusive'
|
||||
@@ -576,7 +629,6 @@ def server_connect():
|
||||
session.authenticate(username, passphrase)
|
||||
while not session.authenticated:
|
||||
username = raw_input("Name: ")
|
||||
readline.clear_history()
|
||||
passphrase = getpass.getpass("Passphrase: ")
|
||||
session.authenticate(username, passphrase)
|
||||
|
||||
@@ -593,15 +645,20 @@ except socket.gaierror:
|
||||
# sys.stdout.write('\x1b[H\x1b[J')
|
||||
# sys.stdout.flush()
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
readline.set_completer(completer)
|
||||
if sys.stdout.isatty():
|
||||
import readline
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.parse_and_bind("set bell-style none")
|
||||
readline.set_completer(completer)
|
||||
|
||||
doexit = False
|
||||
inconsole = False
|
||||
pendingcommand = ""
|
||||
if len(shellargs) == 1 and ' ' not in shellargs[0]: # straight to node console
|
||||
session_node = get_session_node(shellargs)
|
||||
if session_node is not None:
|
||||
consoleonly = True
|
||||
do_command("start /nodes/%s/console/session" % shellargs[0], netserver)
|
||||
do_command("start /nodes/%s/console/session" % session_node, netserver)
|
||||
doexit = True
|
||||
elif shellargs:
|
||||
command = " ".join(shellargs)
|
||||
@@ -642,7 +699,7 @@ while inconsole or not doexit:
|
||||
try:
|
||||
server_connect()
|
||||
connected = True
|
||||
except socket.gaierror, socket.error:
|
||||
except (socket.gaierror, socket.error):
|
||||
pass
|
||||
if not connected:
|
||||
time.sleep(1)
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/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 sys
|
||||
|
||||
confettypath = os.path.join(os.path.dirname(sys.argv[0]), 'confetty')
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] node",
|
||||
epilog="Command sequences are available while connected to a console, hit "
|
||||
"ctrl-'e', then release ctrl, then 'c', then '?' for a full list. "
|
||||
"For example, ctrl-'e', then 'c', then '.' will exit the current "
|
||||
"console")
|
||||
(options, args) = argparser.parse_args()
|
||||
if len(args) != 1:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
os.execl(confettypath, confettypath, 'start',
|
||||
'/nodes/{0}/console/session'.format(args[0]))
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/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.
|
||||
|
||||
from datetime import datetime as dt
|
||||
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
|
||||
|
||||
argparser = optparse.OptionParser(
|
||||
usage="Usage: %prog [options] noderange (clear)")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
deletemode = False
|
||||
if len(sys.argv) == 3:
|
||||
if sys.argv[2] == 'clear':
|
||||
deletemode = True
|
||||
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
|
||||
def format_event(evt):
|
||||
retparts = []
|
||||
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 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 and evt['event'] and evt['event'] is not None:
|
||||
evttext = evt['event']
|
||||
try:
|
||||
if evttext.startswith(evt['component'] + ' - '):
|
||||
evttext = evt['event'].replace(evt['component'] + ' - ', '')
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
dscparts.append(evttext)
|
||||
retparts.append(' - '.join(dscparts))
|
||||
return ' '.join(retparts)
|
||||
|
||||
|
||||
if deletemode:
|
||||
func = session.delete
|
||||
else:
|
||||
func = session.read
|
||||
for rsp in func('/noderange/{0}/events/hardware/log'.format(noderange)):
|
||||
if 'error' in rsp:
|
||||
sys.stderr.write(rsp['error'] + '\n')
|
||||
exitcode |= rsp['errorcode']
|
||||
if 'databynode' in rsp:
|
||||
nodedata = rsp['databynode']
|
||||
for node in nodedata:
|
||||
thisdata = nodedata[node]
|
||||
if 'error' in thisdata:
|
||||
sys.stderr.write('{0}: {1}\n'.format(node, thisdata['error']))
|
||||
exitcode |= 1
|
||||
if 'events' in thisdata:
|
||||
evtdata = thisdata['events']
|
||||
for evt in evtdata:
|
||||
print '{0}: {1}'.format(node, format_event(evt))
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/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 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
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
try:
|
||||
session = client.Command()
|
||||
for res in session.read('/noderange/{0}/inventory/firmware/all/all'.format(
|
||||
noderange)):
|
||||
printerror(res)
|
||||
if 'databynode' not in res:
|
||||
continue
|
||||
for node in res['databynode']:
|
||||
printerror(res['databynode'][node], node)
|
||||
if 'firmware' not in res['databynode'][node]:
|
||||
continue
|
||||
for inv in res['databynode'][node]['firmware']:
|
||||
for prefix in inv:
|
||||
printfirm(node, prefix, inv[prefix])
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(exitcode)
|
||||
@@ -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,6 +16,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import codecs
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -28,42 +29,64 @@ import confluent.client as client
|
||||
|
||||
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
|
||||
|
||||
noderange = sys.argv[1]
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
healthbynode = {}
|
||||
healthexplanations = {}
|
||||
for health in session.read('/noderange/{0}/health/hardware'.format(noderange)):
|
||||
if 'error' in health:
|
||||
sys.stderr.write(health['error'] + '\n')
|
||||
if 'errorcode' in health:
|
||||
exitcode |= health['errorcode']
|
||||
else:
|
||||
exitcode |= 1
|
||||
continue
|
||||
if 'databynode' not in health:
|
||||
continue
|
||||
health = health['databynode']
|
||||
for node in health:
|
||||
if 'health' in health[node]:
|
||||
healthbynode[node] = health[node]['health']['value']
|
||||
if 'sensors' in health[node]:
|
||||
healthexplanations[node] = []
|
||||
for sensor in health[node]['sensors']:
|
||||
explanation = sensor['name'] + ':'
|
||||
if sensor['value'] is not None:
|
||||
explanation += sensor['value']
|
||||
if sensor['units'] is not None:
|
||||
explanation += sensor['units']
|
||||
if sensor['states']:
|
||||
explanation += ','.join(sensor['states'])
|
||||
healthexplanations[node].append(explanation)
|
||||
if node in healthbynode and node in healthexplanations:
|
||||
if healthexplanations[node]:
|
||||
print('{0}: {1} ({2})'.format(
|
||||
node, healthbynode[node],
|
||||
','.join(healthexplanations[node])))
|
||||
|
||||
def main():
|
||||
global session, exitcode, healthbynode, healthexplanations, health, node, sensor, explanation
|
||||
session = client.Command()
|
||||
exitcode = 0
|
||||
healthbynode = {}
|
||||
healthexplanations = {}
|
||||
for health in session.read(
|
||||
'/noderange/{0}/health/hardware'.format(noderange)):
|
||||
if 'error' in health:
|
||||
sys.stderr.write(health['error'] + '\n')
|
||||
if 'errorcode' in health:
|
||||
exitcode |= health['errorcode']
|
||||
else:
|
||||
print('{0}: {1}'.format(node, healthbynode[node]))
|
||||
exitcode |= 1
|
||||
continue
|
||||
if 'databynode' not in health:
|
||||
continue
|
||||
health = health['databynode']
|
||||
for node in health:
|
||||
if 'error' in health[node]:
|
||||
sys.stderr.write('{0}: Error: {1}\n'.format(
|
||||
node, health[node]['error']))
|
||||
exitcode |= 1
|
||||
if 'health' in health[node]:
|
||||
healthbynode[node] = health[node]['health']['value']
|
||||
if 'sensors' in health[node]:
|
||||
healthexplanations[node] = []
|
||||
for sensor in health[node]['sensors']:
|
||||
explanation = sensor['name'] + ':'
|
||||
if sensor['value'] is not None:
|
||||
explanation += str(sensor['value'])
|
||||
if sensor['units'] is not None:
|
||||
explanation += sensor['units']
|
||||
if sensor['states']:
|
||||
explanation += ','
|
||||
if sensor['states']:
|
||||
explanation += ','.join(sensor['states'])
|
||||
healthexplanations[node].append(explanation)
|
||||
if node in healthbynode and node in healthexplanations:
|
||||
if healthexplanations[node]:
|
||||
print(u'{0}: {1} ({2})'.format(
|
||||
node, healthbynode[node],
|
||||
','.join(healthexplanations[node])))
|
||||
else:
|
||||
print('{0}: {1}'.format(node, healthbynode[node]))
|
||||
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@@ -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,6 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -25,8 +26,14 @@ 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]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
noderange = sys.argv[1]
|
||||
identifystate = None
|
||||
if len(sys.argv) > 2:
|
||||
identifystate = sys.argv[2]
|
||||
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/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 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
|
||||
|
||||
|
||||
def pretty(text):
|
||||
if text == 'pcislot':
|
||||
return 'PCI slot'
|
||||
if text == 'partnumber':
|
||||
return 'part number'
|
||||
return text
|
||||
|
||||
def print_mem_info(node, prefix, meminfo):
|
||||
capacity = meminfo['capacity_mb'] / 1024
|
||||
memdescfmt = '{0}GB PC'
|
||||
if meminfo['memory_type'] == 'DDR3 SDRAM':
|
||||
memdescfmt += '3-{1} '
|
||||
elif meminfo['memory_type'] == 'DDR4 SDRAM':
|
||||
memdescfmt += '4-{1} '
|
||||
if meminfo['ecc']:
|
||||
memdescfmt += 'ECC '
|
||||
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
|
||||
|
||||
|
||||
argparser = optparse.OptionParser(usage="Usage: %prog <noderange>")
|
||||
(options, args) = argparser.parse_args()
|
||||
try:
|
||||
noderange = sys.argv[1]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
try:
|
||||
session = client.Command()
|
||||
for res in session.read('/noderange/{0}/inventory/hardware/all/all'.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']:
|
||||
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:
|
||||
print_mem_info(node, prefix, info)
|
||||
continue
|
||||
for datum in info:
|
||||
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)
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2015-2017 Lenovo
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__author__ = 'jjohnson2'
|
||||
|
||||
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
|
||||
|
||||
|
||||
def attrrequested(attr, attrlist, seenattributes):
|
||||
for candidate in attrlist:
|
||||
truename = candidate
|
||||
if candidate.startswith('hm'):
|
||||
candidate = candidate.replace('hm', 'hardwaremanagement', 1)
|
||||
if candidate == attr:
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
elif '.' not in candidate and attr.startswith(candidate + '.'):
|
||||
seenattributes.add(truename)
|
||||
return True
|
||||
return False
|
||||
argparser = optparse.OptionParser(
|
||||
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()
|
||||
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 attrrequested(attr, args[1:], seenattributes):
|
||||
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))
|
||||
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)
|
||||
@@ -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,6 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -25,8 +26,16 @@ 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]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
noderange = sys.argv[1]
|
||||
setstate = None
|
||||
if len(sys.argv) > 2:
|
||||
if setstate == 'softoff':
|
||||
|
||||
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/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)
|
||||
@@ -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,6 +16,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
@@ -39,13 +40,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:
|
||||
@@ -54,8 +57,11 @@ if options.numreadings:
|
||||
repeatmode = options.numreadings
|
||||
if options.interval is None:
|
||||
options.interval = 1
|
||||
|
||||
noderange = args[0]
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
sensors = []
|
||||
for sensorgroup in args[1:]:
|
||||
for sensor in sensorgroup.split(','):
|
||||
@@ -98,6 +104,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)
|
||||
@@ -139,7 +147,12 @@ def sensorpass(showout=True, appendtime=False):
|
||||
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:
|
||||
@@ -190,7 +203,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:
|
||||
@@ -205,4 +219,8 @@ def main():
|
||||
sensorpass(True)
|
||||
|
||||
|
||||
main()
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
sys.exit(0)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -25,12 +26,34 @@ 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')
|
||||
|
||||
noderange = sys.argv[1]
|
||||
(options, args) = argparser.parse_args()
|
||||
|
||||
try:
|
||||
noderange = args[0]
|
||||
except IndexError:
|
||||
argparser.print_help()
|
||||
sys.exit(1)
|
||||
bootdev = None
|
||||
if len(sys.argv) > 2:
|
||||
bootdev = sys.argv[2]
|
||||
if bootdev in ('net', 'pxe'):
|
||||
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))
|
||||
@@ -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.
|
||||
@@ -43,7 +43,7 @@ def _parseserver(string):
|
||||
|
||||
class Command(object):
|
||||
|
||||
def __init__(self, server="/var/run/confluent/api.sock"):
|
||||
def __init__(self, server=None):
|
||||
self.connection = None
|
||||
if server is None:
|
||||
if 'CONFLUENT_HOST' in os.environ:
|
||||
@@ -62,6 +62,10 @@ class Command(object):
|
||||
self.authenticated = True
|
||||
else:
|
||||
self.authenticated = False
|
||||
if not self.authenticated and 'CONFLUENT_USER' in os.environ:
|
||||
username = os.environ['CONFLUENT_USER']
|
||||
passphrase = os.environ['CONFLUENT_PASSPHRASE']
|
||||
self.authenticate(username, passphrase)
|
||||
|
||||
def authenticate(self, username, password):
|
||||
tlvdata.send(self.connection,
|
||||
@@ -93,37 +97,50 @@ class Command(object):
|
||||
return rc
|
||||
|
||||
def simple_noderange_command(self, noderange, resource, input=None,
|
||||
key=None):
|
||||
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('/noderange/{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}):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
return rc
|
||||
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('/noderange/{0}/{1}'.format(
|
||||
noderange, resource)):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
else:
|
||||
kwargs[ikey] = input
|
||||
for res in self.update('/noderange/{0}/{1}'.format(
|
||||
noderange, resource), kwargs):
|
||||
rc = self.handle_results(ikey, rc, res)
|
||||
return rc
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
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):
|
||||
@@ -146,6 +163,7 @@ class Command(object):
|
||||
try:
|
||||
self.connection.settimeout(5)
|
||||
self.connection.connect(sa)
|
||||
self.connection.settimeout(None)
|
||||
except:
|
||||
raise
|
||||
self.connection.close()
|
||||
|
||||
@@ -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):
|
||||
@@ -38,6 +51,9 @@ def send(handle, data):
|
||||
if isinstance(data, str):
|
||||
# plain text, e.g. console data
|
||||
tl = len(data)
|
||||
if tl == 0:
|
||||
# if you don't have anything to say, don't say anything at all
|
||||
return
|
||||
if tl < 16777216:
|
||||
# type for string is '0', so we don't need
|
||||
# to xor anything in
|
||||
@@ -76,6 +92,8 @@ def recv(handle):
|
||||
# 4 byte tlv
|
||||
dlen = tl & 16777215 # grab lower 24 bits
|
||||
datatype = (tl & 2130706432) >> 24 # grab 7 bits from near beginning
|
||||
if dlen == 0:
|
||||
return None
|
||||
data = handle.recv(dlen)
|
||||
while len(data) < dlen:
|
||||
ndata = handle.recv(dlen - len(data))
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
PATH=/opt/confluent/bin:$PATH
|
||||
export PATH
|
||||
@@ -0,0 +1,37 @@
|
||||
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)
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,48 @@
|
||||
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`
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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`
|
||||
@@ -7,5 +7,9 @@ setup(
|
||||
author_email='jjohnson2@lenovo.com',
|
||||
url='http://xcat.sf.net/',
|
||||
packages=['confluent'],
|
||||
scripts=['bin/confetty', 'bin/nodehealth', 'bin/nodeidentify', 'bin/nodepower', 'bin/nodesensors', 'bin/nodesetboot'],
|
||||
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'],
|
||||
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
|
||||
)
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
include sysvinit/*
|
||||
include systemd/*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1.1
|
||||
@@ -22,14 +22,17 @@ 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)
|
||||
from confluent import main
|
||||
import confluent.main
|
||||
|
||||
#import cProfile
|
||||
#import time
|
||||
#p = cProfile.Profile(time.clock)
|
||||
#p.enable()
|
||||
#try:
|
||||
main.run()
|
||||
import multiprocessing
|
||||
if __name__ == '__main__':
|
||||
multiprocessing.freeze_support()
|
||||
confluent.main.run()
|
||||
#except:
|
||||
# pass
|
||||
#p.disable()
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
#
|
||||
# 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 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.main
|
||||
|
||||
#import cProfile
|
||||
#import time
|
||||
#p = cProfile.Profile(time.clock)
|
||||
#p.enable()
|
||||
#try:
|
||||
import multiprocessing
|
||||
if __name__ == '__main__':
|
||||
multiprocessing.freeze_support()
|
||||
confluent.main.run()
|
||||
#except:
|
||||
# pass
|
||||
#p.disable()
|
||||
#p.print_stats(sort='cumulative')
|
||||
#p.print_stats(sort='time')
|
||||
@@ -0,0 +1,59 @@
|
||||
# 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.
|
||||
|
||||
# This handles incoming unsolicited alerts over the network. For the moment
|
||||
# we'll link into ipmi.py to do PET alerts, with the assumption that more
|
||||
# typical .mib based handling will be used for other events and confluent's
|
||||
# event service is to handle the peculiarities of an IPMI PET. In the future
|
||||
# it may make sense to extend this in a more general case, but that rework can
|
||||
# be deferred for now.
|
||||
|
||||
# Phase 1 is to be facilitating some application doing http calls to get help
|
||||
# decoding data.
|
||||
|
||||
# Phase 2 is to be able to bind a port and have snmptrapd just forward the
|
||||
# packet rather than have something block snmptrapd at all for things confluent
|
||||
# can handle.
|
||||
|
||||
__author__ = 'jjohnson2'
|
||||
|
||||
import confluent.exceptions as exc
|
||||
import confluent.lookuptools as lookuptools
|
||||
import confluent.core
|
||||
|
||||
def decode_alert(varbinds, configmanager):
|
||||
"""Decode an SNMP alert for a server
|
||||
|
||||
Given the agentaddr, OID for the trap, and a dict of varbinds,
|
||||
ascertain the node identity and then request a decode
|
||||
|
||||
:param varbinds: A dictionary of OID to value varbinds. Also supported
|
||||
are special keywords 'enterprise' and 'specificTrap' for
|
||||
SNMPv1 traps.
|
||||
|
||||
"""
|
||||
try:
|
||||
agentaddr = varbinds['.1.3.6.1.6.3.18.1.3.0']
|
||||
except KeyError:
|
||||
agentaddr = varbinds['1.3.6.1.6.3.18.1.3.0']
|
||||
node = lookuptools.node_by_manager(agentaddr)
|
||||
if node is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Unable to find a node with specified manager')
|
||||
return confluent.core.handle_path(
|
||||
'/nodes/{0}/events/hardware/decode'.format(node), 'update',
|
||||
configmanager, varbinds, autostrip=False)
|
||||
|
||||
@@ -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
|
||||
@@ -26,7 +26,10 @@ import Crypto.Protocol.KDF as KDF
|
||||
import hashlib
|
||||
import hmac
|
||||
import multiprocessing
|
||||
import PAM
|
||||
try:
|
||||
import PAM
|
||||
except ImportError:
|
||||
pass
|
||||
import time
|
||||
|
||||
_pamservice = 'confluent'
|
||||
@@ -113,7 +116,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)
|
||||
@@ -161,6 +164,8 @@ def check_user_passphrase(name, passphrase, element=None, tenant=False):
|
||||
pammy.acct_mgmt()
|
||||
del pammy
|
||||
return authorize(user, element, tenant, skipuserobj=False)
|
||||
except NameError:
|
||||
pass
|
||||
except PAM.error:
|
||||
if credobj.haspam:
|
||||
return None
|
||||
@@ -173,7 +178,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
|
||||
|
||||
@@ -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.
|
||||
@@ -80,6 +81,18 @@ node = {
|
||||
#'id': {
|
||||
# 'description': ('Numeric identifier for node')
|
||||
#},
|
||||
'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 +188,70 @@ 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",
|
||||
'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'],
|
||||
# },
|
||||
# 'enclosure.bay': {
|
||||
# 'description': 'The bay in the enclosure, if any',
|
||||
},
|
||||
'enclosure.bay': {
|
||||
'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'],
|
||||
# },
|
||||
# 'inventory.serialnumber': {
|
||||
# 'id.serial': {
|
||||
# 'description': 'The manufacturer serial number of node',
|
||||
# },
|
||||
# 'inventory.uuid': {
|
||||
# 'id.uuid': {
|
||||
# 'description': 'The UUID of the node as presented in DMI',
|
||||
# },
|
||||
# 'inventory.modelnumber': {
|
||||
# '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 +262,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.'),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2014 IBM Corporation
|
||||
#
|
||||
# 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 defines config variable to store the global configuration for confluent
|
||||
import ConfigParser
|
||||
import os
|
||||
|
||||
_config = None
|
||||
|
||||
|
||||
def init_config():
|
||||
global _config
|
||||
configfile = "/etc/confluent/service.cfg"
|
||||
if os.name == 'nt':
|
||||
configfile = os.path.join(os.getenv('SystemDrive'), '\\ProgramData',
|
||||
'confluent', 'cfg', 'service.cfg')
|
||||
_config = ConfigParser.ConfigParser()
|
||||
_config.read(configfile)
|
||||
|
||||
|
||||
def get_config():
|
||||
if _config is None:
|
||||
init_config()
|
||||
return _config
|
||||
|
||||
|
||||
def get_int_option(section, option):
|
||||
if _config is None:
|
||||
init_config()
|
||||
try:
|
||||
return _config.getint(section, option)
|
||||
except (
|
||||
ConfigParser.NoSectionError, ConfigParser.NoOptionError,
|
||||
ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_boolean_option(section, option):
|
||||
if _config is None:
|
||||
init_config()
|
||||
try:
|
||||
return _config.getboolean(section, option)
|
||||
except (
|
||||
ConfigParser.NoSectionError, ConfigParser.NoOptionError,
|
||||
ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def get_option(section, option):
|
||||
if _config is None:
|
||||
init_config()
|
||||
try:
|
||||
return _config.get(section, option)
|
||||
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
|
||||
return None
|
||||
@@ -67,6 +67,7 @@ import base64
|
||||
import confluent.config.attributes as allattributes
|
||||
import confluent.log
|
||||
import confluent.util
|
||||
import confluent.exceptions as exc
|
||||
import copy
|
||||
import cPickle
|
||||
import errno
|
||||
@@ -78,6 +79,7 @@ import re
|
||||
import string
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
|
||||
|
||||
_masterkey = None
|
||||
@@ -104,22 +106,26 @@ def _derive_keys(password, salt):
|
||||
lambda p, s: HMAC.new(p, s, SHA256).digest())
|
||||
finalkey = KDF.PBKDF2(tmpkey, salt, 32, 50000,
|
||||
lambda p, s: HMAC.new(p, s, SHA256).digest())
|
||||
return finalkey[:32], finalkey[32:]
|
||||
return finalkey[:16], finalkey[16:]
|
||||
|
||||
|
||||
def _get_protected_key(keydict, password):
|
||||
if keydict['unencryptedvalue']:
|
||||
def _get_protected_key(keydict, password, paramname):
|
||||
if password and 'unencryptedvalue' in keydict:
|
||||
set_global(paramname, _format_key(
|
||||
keydict['unencryptedvalue'],
|
||||
password=password))
|
||||
if 'unencryptedvalue' in keydict:
|
||||
return keydict['unencryptedvalue']
|
||||
# TODO(jbjohnso): check for TPM sealing
|
||||
if 'passphraseprotected' in keydict:
|
||||
if password is None:
|
||||
raise Exception("Passphrase protected secret requires password")
|
||||
for pp in keydict['passphraseprotected']:
|
||||
salt = pp[0]
|
||||
privkey, integkey = _derive_keys(password, salt)
|
||||
return decrypt_value(pp[1:], key=privkey, integritykey=integkey)
|
||||
raise exc.LockedCredentials("Passphrase protected secret requires password")
|
||||
pp = keydict['passphraseprotected']
|
||||
salt = pp[0]
|
||||
privkey, integkey = _derive_keys(password, salt)
|
||||
return decrypt_value(pp[1:], key=privkey, integritykey=integkey)
|
||||
else:
|
||||
raise Exception("No available decryption key")
|
||||
raise exc.LockedCredentials("No available decryption key")
|
||||
|
||||
|
||||
def _format_key(key, password=None):
|
||||
@@ -127,7 +133,7 @@ def _format_key(key, password=None):
|
||||
salt = os.urandom(32)
|
||||
privkey, integkey = _derive_keys(password, salt)
|
||||
cval = crypt_value(key, key=privkey, integritykey=integkey)
|
||||
return {"passphraseprotected": cval}
|
||||
return {"passphraseprotected": (salt,) + cval}
|
||||
else:
|
||||
return {"unencryptedvalue": key}
|
||||
|
||||
@@ -138,7 +144,7 @@ def init_masterkey(password=None):
|
||||
cfgn = get_global('master_privacy_key')
|
||||
|
||||
if cfgn:
|
||||
_masterkey = _get_protected_key(cfgn, password=password)
|
||||
_masterkey = _get_protected_key(cfgn, password, 'master_privacy_key')
|
||||
else:
|
||||
_masterkey = os.urandom(32)
|
||||
set_global('master_privacy_key', _format_key(
|
||||
@@ -146,7 +152,8 @@ def init_masterkey(password=None):
|
||||
password=password))
|
||||
cfgn = get_global('master_integrity_key')
|
||||
if cfgn:
|
||||
_masterintegritykey = _get_protected_key(cfgn, password=password)
|
||||
_masterintegritykey = _get_protected_key(cfgn, password,
|
||||
'master_integrity_key')
|
||||
else:
|
||||
_masterintegritykey = os.urandom(64)
|
||||
set_global('master_integrity_key', _format_key(
|
||||
@@ -155,15 +162,18 @@ def init_masterkey(password=None):
|
||||
|
||||
|
||||
def decrypt_value(cryptvalue,
|
||||
key=_masterkey,
|
||||
integritykey=_masterintegritykey):
|
||||
key=None,
|
||||
integritykey=None):
|
||||
iv, cipherdata, hmac = cryptvalue
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
init_masterkey()
|
||||
check_hmac = HMAC.new(_masterintegritykey, cipherdata, SHA256).digest()
|
||||
if key is None and integritykey is None:
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
init_masterkey()
|
||||
key = _masterkey
|
||||
integritykey = _masterintegritykey
|
||||
check_hmac = HMAC.new(integritykey, cipherdata, SHA256).digest()
|
||||
if hmac != check_hmac:
|
||||
raise Exception("bad HMAC value on crypted value")
|
||||
decrypter = AES.new(_masterkey, AES.MODE_CBC, iv)
|
||||
decrypter = AES.new(key, AES.MODE_CBC, iv)
|
||||
value = decrypter.decrypt(cipherdata)
|
||||
padsize = ord(value[-1])
|
||||
pad = value[-padsize:]
|
||||
@@ -176,13 +186,14 @@ def decrypt_value(cryptvalue,
|
||||
|
||||
|
||||
def crypt_value(value,
|
||||
key=_masterkey,
|
||||
integritykey=_masterintegritykey):
|
||||
key=None,
|
||||
integritykey=None):
|
||||
# encrypt given value
|
||||
# PKCS7 is the padding scheme to employ, if no padded needed, pad with 16
|
||||
# check HMAC prior to attempting decrypt
|
||||
if key is None or integritykey is None:
|
||||
init_masterkey()
|
||||
if _masterkey is None or _masterintegritykey is None:
|
||||
init_masterkey()
|
||||
key = _masterkey
|
||||
integritykey = _masterintegritykey
|
||||
iv = os.urandom(16)
|
||||
@@ -203,8 +214,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.iterkeys():
|
||||
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
|
||||
|
||||
@@ -405,7 +422,11 @@ def hook_new_configmanagers(callback):
|
||||
|
||||
|
||||
class ConfigManager(object):
|
||||
_cfgdir = "/etc/confluent/cfg/"
|
||||
if os.name == 'nt':
|
||||
_cfgdir = os.path.join(
|
||||
os.getenv('SystemDrive'), '\\ProgramData', 'confluent', 'cfg')
|
||||
else:
|
||||
_cfgdir = "/etc/confluent/cfg"
|
||||
_cfgwriter = None
|
||||
_writepending = False
|
||||
_syncrunning = False
|
||||
@@ -414,9 +435,10 @@ class ConfigManager(object):
|
||||
_nodecollwatchers = {}
|
||||
_notifierids = {}
|
||||
|
||||
def __init__(self, tenant, decrypt=False):
|
||||
def __init__(self, tenant, decrypt=False, username=None):
|
||||
global _cfgstore
|
||||
self.decrypt = decrypt
|
||||
self.current_user = username
|
||||
if tenant is None:
|
||||
self.tenant = None
|
||||
if 'main' not in _cfgstore:
|
||||
@@ -739,7 +761,17 @@ 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):
|
||||
@@ -761,7 +793,7 @@ class ConfigManager(object):
|
||||
# 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
|
||||
|
||||
@@ -864,6 +896,9 @@ class ConfigManager(object):
|
||||
def set_group_attributes(self, attribmap, autocreate=False):
|
||||
changeset = {}
|
||||
for group in attribmap.iterkeys():
|
||||
if group == '':
|
||||
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():
|
||||
@@ -973,7 +1008,16 @@ class ConfigManager(object):
|
||||
if node not in attribwatchers:
|
||||
continue
|
||||
attribwatcher = attribwatchers[node]
|
||||
for attrname in nodeattrs[node].iterkeys():
|
||||
# usually, we will only look at the specific attribute keys that
|
||||
# have had change flagged, so set up to iterate through only those
|
||||
checkattrs = nodeattrs[node]
|
||||
if '_nodedeleted' in nodeattrs[node]:
|
||||
# in the case of a deleted node, we want to iterate through
|
||||
# *all* attributes that the node might have had set prior
|
||||
# to deletion, to make all watchers aware of the removed
|
||||
# node and take appropriate action
|
||||
checkattrs = attribwatcher
|
||||
for attrname in checkattrs:
|
||||
if attrname not in attribwatcher:
|
||||
continue
|
||||
for notifierid in attribwatcher[attrname].iterkeys():
|
||||
@@ -1006,6 +1050,9 @@ class ConfigManager(object):
|
||||
watcher(added=[], deleting=nodes, configmanager=self)
|
||||
changeset = {}
|
||||
for node in nodes:
|
||||
# set a reserved attribute for the sake of the change notification
|
||||
# framework to trigger on
|
||||
changeset[node] = {'_nodedeleted': 1}
|
||||
node = node.encode('utf-8')
|
||||
if node in self._cfgstore['nodes']:
|
||||
self._sync_groups_to_node(node=node, groups=[],
|
||||
@@ -1070,6 +1117,8 @@ class ConfigManager(object):
|
||||
# this mitigates risk of arguments being partially applied
|
||||
for node in attribmap.iterkeys():
|
||||
node = node.encode('utf-8')
|
||||
if node == '':
|
||||
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():
|
||||
@@ -1192,23 +1241,27 @@ class ConfigManager(object):
|
||||
global _cfgstore
|
||||
_cfgstore = {}
|
||||
rootpath = cls._cfgdir
|
||||
_load_dict_from_dbm(['globals'], rootpath + "/globals")
|
||||
_load_dict_from_dbm(['globals'], os.path.join(rootpath, "globals"))
|
||||
for confarea in _config_areas:
|
||||
_load_dict_from_dbm(['main', confarea], rootpath + "/" + confarea)
|
||||
_load_dict_from_dbm(['main', confarea], os.path.join(rootpath, confarea))
|
||||
try:
|
||||
for tenant in os.listdir(rootpath + '/tenants/'):
|
||||
for tenant in os.listdir(os.path.join(rootpath, 'tenants')):
|
||||
for confarea in _config_areas:
|
||||
_load_dict_from_dbm(
|
||||
['main', tenant, confarea],
|
||||
"%s/%s/%s" % (rootpath, tenant, confarea))
|
||||
os.path.join(rootpath, tenant, confarea))
|
||||
except OSError:
|
||||
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
|
||||
@@ -1231,7 +1284,7 @@ class ConfigManager(object):
|
||||
dirtyglobals = copy.deepcopy(_cfgstore['dirtyglobals'])
|
||||
del _cfgstore['dirtyglobals']
|
||||
_mkpath(cls._cfgdir)
|
||||
globalf = dbm.open(cls._cfgdir + "/globals", 'c', 384) # 0600
|
||||
globalf = dbm.open(os.path.join(cls._cfgdir, "globals"), 'c', 384) # 0600
|
||||
try:
|
||||
for globalkey in dirtyglobals:
|
||||
if globalkey in _cfgstore['globals']:
|
||||
@@ -1252,11 +1305,11 @@ class ConfigManager(object):
|
||||
pathname = cls._cfgdir
|
||||
currdict = _cfgstore['main']
|
||||
else:
|
||||
pathname = cls._cfgdir + '/tenants/' + tenant + '/'
|
||||
pathname = os.path.join(cls._cfgdir, 'tenants', tenant)
|
||||
currdict = _cfgstore['tenant'][tenant]
|
||||
for category in dkdict.iterkeys():
|
||||
_mkpath(pathname)
|
||||
dbf = dbm.open(pathname + category, 'c', 384) # 0600
|
||||
dbf = dbm.open(os.path.join(pathname, category), 'c', 384) # 0600
|
||||
try:
|
||||
for ck in dkdict[category]:
|
||||
if ck not in currdict[category]:
|
||||
@@ -1310,7 +1363,8 @@ def dump_db_to_directory(location, password, redact=None):
|
||||
cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact))
|
||||
cfgfile.write('\n')
|
||||
try:
|
||||
for tenant in os.listdir(ConfigManager._cfgdir + '/tenants/'):
|
||||
for tenant in os.listdir(
|
||||
os.path.join(ConfigManager._cfgdir, '/tenants/')):
|
||||
with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile:
|
||||
cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json(
|
||||
redact=redact))
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
# 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 collections
|
||||
import confluent.config.configmanager as configmodule
|
||||
import confluent.exceptions as exc
|
||||
@@ -35,26 +35,30 @@ import traceback
|
||||
|
||||
_handled_consoles = {}
|
||||
|
||||
_genwatchattribs = frozenset(('console.method', 'console.logging'))
|
||||
|
||||
_tracelog = None
|
||||
|
||||
|
||||
class _ConsoleHandler(object):
|
||||
class ConsoleHandler(object):
|
||||
_plugin_path = '/nodes/{0}/_console/session'
|
||||
_logtobuffer = True
|
||||
_genwatchattribs = frozenset(('console.method', 'console.logging'))
|
||||
|
||||
def __init__(self, node, configmanager):
|
||||
self._dologging = True
|
||||
self._isondemand = False
|
||||
self.error = None
|
||||
self.rcpts = {}
|
||||
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.livesessions = set([])
|
||||
if self._logtobuffer:
|
||||
self.logger = log.Logger(node, console=True,
|
||||
tenant=configmanager.tenant)
|
||||
(text, termstate, timestamp) = self.logger.read_recent_text(8192)
|
||||
else:
|
||||
(text, termstate, timestamp) = ('', 0, False)
|
||||
# when reading from log file, we will use wall clock
|
||||
# it should usually match walltime.
|
||||
self.lasttime = 0
|
||||
@@ -79,10 +83,12 @@ 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 check_isondemand(self):
|
||||
@@ -134,7 +140,7 @@ class _ConsoleHandler(object):
|
||||
self._ondemand()
|
||||
if logvalue == 'none':
|
||||
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):
|
||||
@@ -166,7 +172,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,8 +192,10 @@ 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:
|
||||
self._console = None
|
||||
except:
|
||||
_tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
@@ -202,11 +210,12 @@ 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:
|
||||
@@ -214,7 +223,7 @@ class _ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 30 + (30 * random.random())
|
||||
retrytime = 120 + (120 * random.random())
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -223,7 +232,7 @@ class _ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 30 + (30 * random.random())
|
||||
retrytime = 120 + (120 * random.random())
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -234,7 +243,7 @@ class _ConsoleHandler(object):
|
||||
self.connectstate = 'unconnected'
|
||||
self._send_rcpts({'connectstate': self.connectstate,
|
||||
'error': self.error})
|
||||
retrytime = 30 + (30 * random.random())
|
||||
retrytime = 120 + (120 * random.random())
|
||||
if not self.reconnect:
|
||||
self.reconnect = eventlet.spawn_after(retrytime, self._connect)
|
||||
return
|
||||
@@ -248,11 +257,12 @@ class _ConsoleHandler(object):
|
||||
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()
|
||||
|
||||
@@ -268,32 +278,6 @@ 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 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
|
||||
if self.connectstate == 'unconnected':
|
||||
# if console is not connected, take time to try to assert
|
||||
# connectivity now.
|
||||
if self.reconnect:
|
||||
# cancel an automated retry if one is pending
|
||||
self.reconnect.cancel()
|
||||
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
|
||||
@@ -305,39 +289,44 @@ class _ConsoleHandler(object):
|
||||
# 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 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=username, ltype=log.DataTypes.event,
|
||||
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.
|
||||
if self.reconnect:
|
||||
# cancel an automated retry if one is pending
|
||||
self.reconnect.cancel()
|
||||
self.reconnect = None
|
||||
self.connectstate = 'connecting'
|
||||
eventlet.spawn(self._connect)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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,
|
||||
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()
|
||||
@@ -347,6 +336,9 @@ class _ConsoleHandler(object):
|
||||
if data == conapi.ConsoleEvent.Disconnect:
|
||||
self._got_disconnected()
|
||||
return
|
||||
elif data == '':
|
||||
# ignore empty strings from a cconsole provider
|
||||
return
|
||||
if '\x1b[?1l' in data: # request for ansi mode cursor keys
|
||||
self.appmodedetected = False
|
||||
if '\x1b[?1h' in data: # remember the session wants the client to use
|
||||
@@ -362,8 +354,11 @@ class _ConsoleHandler(object):
|
||||
eventdata |= 2
|
||||
self.log(data, eventdata=eventdata)
|
||||
self.lasttime = util.monotonic_time()
|
||||
self.buffer += data
|
||||
#TODO: analyze buffer for registered events, examples:
|
||||
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:
|
||||
# panics
|
||||
# certificate signing request
|
||||
if len(self.buffer) > 16384:
|
||||
@@ -371,39 +366,40 @@ class _ConsoleHandler(object):
|
||||
self._send_rcpts(data)
|
||||
|
||||
def _send_rcpts(self, data):
|
||||
for rcpt in self.rcpts.itervalues():
|
||||
for rcpt in 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 = ''
|
||||
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
|
||||
# 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
|
||||
# another scheme is the 2J scheme
|
||||
bufidx = self.buffer.rfind('\x1b[2J')
|
||||
if bufidx >= 0:
|
||||
# there was some sort of clear screen event
|
||||
@@ -411,8 +407,8 @@ class _ConsoleHandler(object):
|
||||
# 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
|
||||
# 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
|
||||
|
||||
def write(self, data):
|
||||
@@ -450,13 +446,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
|
||||
|
||||
|
||||
@@ -468,7 +466,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):
|
||||
@@ -476,30 +481,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):
|
||||
@@ -509,13 +525,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
|
||||
|
||||
@@ -523,19 +547,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:
|
||||
@@ -544,16 +575,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
|
||||
|
||||
@@ -33,18 +33,23 @@
|
||||
# functions. Console is special and just get's passed through
|
||||
# see API.txt
|
||||
|
||||
import confluent.alerts as alerts
|
||||
import confluent.config.attributes as attrscheme
|
||||
import confluent.interface.console as console
|
||||
import confluent.exceptions as exc
|
||||
import confluent.messages as msg
|
||||
import confluent.noderange as noderange
|
||||
import confluent.shellmodule as shellmodule
|
||||
try:
|
||||
import confluent.shellmodule as shellmodule
|
||||
except ImportError:
|
||||
pass
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
|
||||
pluginmap = {}
|
||||
|
||||
|
||||
def seek_element(currplace, currkey):
|
||||
try:
|
||||
return currplace[currkey]
|
||||
@@ -55,15 +60,17 @@ def seek_element(currplace, currkey):
|
||||
return currplace
|
||||
raise
|
||||
|
||||
|
||||
def nested_lookup(nestdict, key):
|
||||
try:
|
||||
return reduce(seek_element, key, nestdict)
|
||||
except TypeError as e:
|
||||
except TypeError:
|
||||
raise exc.NotFoundException("Invalid element requested")
|
||||
|
||||
|
||||
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()
|
||||
@@ -71,8 +78,8 @@ def load_plugins():
|
||||
plugindir = os.path.join(plugintop, plugindir)
|
||||
if not os.path.isdir(plugindir):
|
||||
continue
|
||||
sys.path.append(plugindir)
|
||||
#two passes, to avoid adding both py and pyc files
|
||||
sys.path.insert(1, plugindir)
|
||||
# two passes, to avoid adding both py and pyc files
|
||||
for plugin in os.listdir(plugindir):
|
||||
if plugin.startswith('.'):
|
||||
continue
|
||||
@@ -80,7 +87,7 @@ def load_plugins():
|
||||
if plugtype == '.sh':
|
||||
pluginmap[plugin] = shellmodule.Plugin(
|
||||
os.path.join(plugindir, plugin + '.sh'))
|
||||
else:
|
||||
elif "__init__" not in plugin:
|
||||
plugins.add(plugin)
|
||||
for plugin in plugins:
|
||||
tmpmod = __import__(plugin)
|
||||
@@ -89,85 +96,180 @@ 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/']
|
||||
rootcollections = ['noderange/', 'nodes/', 'nodegroups/', 'users/', 'events/']
|
||||
|
||||
|
||||
class PluginRoute(object):
|
||||
def __init__(self, routedict):
|
||||
self.routeinfo = routedict
|
||||
|
||||
|
||||
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 = {
|
||||
'_console': {
|
||||
'session': PluginRoute({
|
||||
'pluginattrs': ['console.method'],
|
||||
}),
|
||||
},
|
||||
'console': {
|
||||
#this is a dummy value, http or socket must handle special
|
||||
'session': PluginRoute({}),
|
||||
},
|
||||
'power': {
|
||||
'state': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'health': {
|
||||
'hardware': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'identify': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'boot': {
|
||||
'nextdevice': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'attributes': {
|
||||
'all': PluginRoute({'handler': 'attributes'}),
|
||||
'current': PluginRoute({'handler': 'attributes'}),
|
||||
},
|
||||
'sensors': {
|
||||
'hardware': {
|
||||
'all': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'temperature': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'power': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'fans': 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',
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
'_console': {
|
||||
'session': PluginRoute({
|
||||
'pluginattrs': ['console.method'],
|
||||
}),
|
||||
},
|
||||
'_shell': {
|
||||
'session': PluginRoute({
|
||||
# For now, not configurable, wait until there's demand
|
||||
'handler': 'ssh',
|
||||
}),
|
||||
},
|
||||
'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',
|
||||
}),
|
||||
},
|
||||
'events': {
|
||||
'hardware': {
|
||||
'log': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'decode': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
'health': {
|
||||
'hardware': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'identify': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
'inventory': {
|
||||
'hardware': {
|
||||
'all': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'firmware': {
|
||||
'all': PluginCollection({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
},
|
||||
'power': {
|
||||
'state': PluginRoute({
|
||||
'pluginattrs': ['hardwaremanagement.method'],
|
||||
'default': 'ipmi',
|
||||
}),
|
||||
},
|
||||
'sensors': {
|
||||
'hardware': {
|
||||
'all': 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):
|
||||
@@ -313,6 +415,7 @@ def enumerate_collections(collections):
|
||||
def handle_nodegroup_request(configmanager, inputdata,
|
||||
pathcomponents, operation):
|
||||
iscollection = False
|
||||
routespec = None
|
||||
if len(pathcomponents) < 2:
|
||||
if operation == "create":
|
||||
inputdata = msg.InputAttributes(pathcomponents, inputdata)
|
||||
@@ -337,10 +440,10 @@ def handle_nodegroup_request(configmanager, inputdata,
|
||||
if iscollection:
|
||||
if operation == "delete":
|
||||
return delete_nodegroup_collection(pathcomponents,
|
||||
configmanager)
|
||||
configmanager)
|
||||
elif operation == "retrieve":
|
||||
return enumerate_nodegroup_collection(pathcomponents,
|
||||
configmanager)
|
||||
configmanager)
|
||||
else:
|
||||
raise Exception("TODO")
|
||||
plugroute = routespec.routeinfo
|
||||
@@ -356,7 +459,7 @@ def handle_nodegroup_request(configmanager, inputdata,
|
||||
|
||||
|
||||
def handle_node_request(configmanager, inputdata, operation,
|
||||
pathcomponents):
|
||||
pathcomponents, autostrip=True):
|
||||
iscollection = False
|
||||
routespec = None
|
||||
if pathcomponents[0] == 'noderange':
|
||||
@@ -397,7 +500,8 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
except TypeError:
|
||||
allnodes.sort()
|
||||
return iterate_collections(allnodes)
|
||||
if isnoderange and len(pathcomponents) == 3 and pathcomponents[2] == 'nodes':
|
||||
if (isnoderange and len(pathcomponents) == 3 and
|
||||
pathcomponents[2] == 'nodes'):
|
||||
# this means that it's a list of relevant nodes
|
||||
nodes = list(nodes)
|
||||
try:
|
||||
@@ -416,6 +520,8 @@ 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)
|
||||
@@ -427,15 +533,20 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
passvalues = []
|
||||
plugroute = routespec.routeinfo
|
||||
inputdata = msg.get_input_message(
|
||||
pathcomponents, operation, inputdata, nodes)
|
||||
pathcomponents, operation, inputdata, nodes, isnoderange)
|
||||
if 'handler' in plugroute: # fixed handler definition, easy enough
|
||||
hfunc = getattr(pluginmap[plugroute['handler']], operation)
|
||||
passvalue =hfunc(
|
||||
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:
|
||||
@@ -455,19 +566,29 @@ def handle_node_request(configmanager, inputdata, operation,
|
||||
nodesbyhandler[hfunc].append(node)
|
||||
else:
|
||||
nodesbyhandler[hfunc] = [node]
|
||||
for hfunc in nodesbyhandler.iterkeys():
|
||||
for hfunc in nodesbyhandler:
|
||||
passvalues.append(hfunc(
|
||||
nodes=nodesbyhandler[hfunc], element=pathcomponents,
|
||||
configmanager=configmanager,
|
||||
inputdata=inputdata))
|
||||
if isnoderange:
|
||||
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()
|
||||
|
||||
def handle_path(path, operation, configmanager, inputdata=None):
|
||||
# elif isinstance(passvalues[0], console.Console):
|
||||
# return passvalues[0]
|
||||
# else:
|
||||
# return stripnode(passvalues[0], nodes[0])
|
||||
|
||||
|
||||
def handle_path(path, operation, configmanager, inputdata=None, autostrip=True):
|
||||
"""Given a full path request, return an object.
|
||||
|
||||
The plugins should generally return some sort of iterator.
|
||||
@@ -482,17 +603,17 @@ def handle_path(path, operation, configmanager, inputdata=None):
|
||||
return enumerate_collections(rootcollections)
|
||||
elif pathcomponents[0] == 'noderange':
|
||||
return handle_node_request(configmanager, inputdata, operation,
|
||||
pathcomponents)
|
||||
pathcomponents, autostrip)
|
||||
elif pathcomponents[0] == 'nodegroups':
|
||||
return handle_nodegroup_request(configmanager, inputdata,
|
||||
pathcomponents,
|
||||
operation)
|
||||
pathcomponents,
|
||||
operation)
|
||||
elif pathcomponents[0] == 'nodes':
|
||||
#single node request of some sort
|
||||
# single node request of some sort
|
||||
return handle_node_request(configmanager, inputdata,
|
||||
operation, pathcomponents)
|
||||
operation, pathcomponents, autostrip)
|
||||
elif pathcomponents[0] == 'users':
|
||||
#TODO: when non-administrator accounts exist,
|
||||
# TODO: when non-administrator accounts exist,
|
||||
# they must only be allowed to see their own user
|
||||
try:
|
||||
user = pathcomponents[1]
|
||||
@@ -514,5 +635,16 @@ def handle_path(path, operation, configmanager, inputdata=None):
|
||||
pathcomponents, operation, inputdata)
|
||||
update_user(user, inputdata.attribs, configmanager)
|
||||
return show_user(user, configmanager)
|
||||
elif pathcomponents[0] == 'events':
|
||||
try:
|
||||
element = pathcomponents[1]
|
||||
except IndexError:
|
||||
if operation != 'retrieve':
|
||||
raise exc.InvalidArgumentException('Target is read-only')
|
||||
return (msg.ChildCollection('decode'),)
|
||||
if element != 'decode':
|
||||
raise exc.NotFoundException()
|
||||
if operation == 'update':
|
||||
return alerts.decode_alert(inputdata, configmanager)
|
||||
else:
|
||||
raise exc.NotFoundException()
|
||||
|
||||
@@ -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.
|
||||
@@ -14,41 +15,92 @@
|
||||
# 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 })
|
||||
|
||||
|
||||
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 = 'Request path not recognized'
|
||||
|
||||
|
||||
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
|
||||
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
|
||||
apierrorstr = 'Global configuration contains an error'
|
||||
|
||||
|
||||
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
|
||||
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}'
|
||||
|
||||
@@ -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.
|
||||
@@ -25,22 +25,29 @@ import confluent.exceptions as exc
|
||||
import confluent.log as log
|
||||
import confluent.messages
|
||||
import confluent.core as pluginapi
|
||||
import confluent.tlvdata as tlvdata
|
||||
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
|
||||
import eventlet.wsgi
|
||||
#scgi = eventlet.import_patched('flup.server.scgi')
|
||||
tlvdata = confluent.tlvdata
|
||||
|
||||
|
||||
auditlog = None
|
||||
tracelog = None
|
||||
consolesessions = {}
|
||||
confluent.asynchttp.set_console_sessions(consolesessions)
|
||||
httpsessions = {}
|
||||
opmap = {
|
||||
'POST': 'create',
|
||||
@@ -103,21 +110,54 @@ def node_creation_resources():
|
||||
desc=attribs.node[attr]['description']).html() + '<br>\n'
|
||||
|
||||
|
||||
def user_creation_resources():
|
||||
credential = {
|
||||
'uid': {
|
||||
'description': (''),
|
||||
},
|
||||
'username': {
|
||||
'description': (''),
|
||||
},
|
||||
'password': {
|
||||
'description': (''),
|
||||
},
|
||||
'privilege_level': {
|
||||
'description': (''),
|
||||
},
|
||||
}
|
||||
for attr in sorted(credential.iterkeys()):
|
||||
if attr == "password":
|
||||
yield confluent.messages.CryptedAttributes(
|
||||
kv={attr: None},
|
||||
desc=credential[attr]['description']).html() + '<br>\n'
|
||||
else:
|
||||
yield confluent.messages.Attributes(
|
||||
kv={attr: None},
|
||||
desc=credential[attr]['description']).html() + '<br>\n'
|
||||
|
||||
|
||||
create_resource_functions = {
|
||||
'/nodes/': node_creation_resources,
|
||||
'/groups/': group_creation_resources,
|
||||
'nodes': node_creation_resources,
|
||||
'groups': group_creation_resources,
|
||||
'users': user_creation_resources,
|
||||
}
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
@@ -155,6 +195,63 @@ 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_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
|
||||
@@ -162,6 +259,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
|
||||
@@ -169,13 +267,31 @@ 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)
|
||||
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)
|
||||
@@ -185,14 +301,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,
|
||||
@@ -208,8 +325,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}
|
||||
@@ -235,7 +356,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', ''
|
||||
@@ -243,7 +364,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}
|
||||
@@ -266,6 +387,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'), ('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:
|
||||
@@ -277,30 +405,56 @@ 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 (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 = {
|
||||
@@ -315,10 +469,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"
|
||||
@@ -327,6 +496,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
|
||||
@@ -341,14 +512,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
|
||||
@@ -377,12 +585,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,
|
||||
@@ -393,21 +612,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:
|
||||
start_response('504 Unreachable Target', headers)
|
||||
yield '504 - Unreachable Target'
|
||||
except exc.TargetEndpointBadCredentials:
|
||||
start_response('502 Bad Credentials', headers)
|
||||
yield '502 - Bad Credentials'
|
||||
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>' \
|
||||
@@ -429,7 +641,6 @@ def _assemble_html(responses, resource, querydict, url, extension):
|
||||
iscollection = True
|
||||
yield '<a rel="collection" href="../{0}">../{0}</a><br>'.format(
|
||||
extension)
|
||||
|
||||
else:
|
||||
iscollection = False
|
||||
yield '<a rel="collection" href="./{0}">./{0}</a><br>'.format(
|
||||
@@ -445,11 +656,15 @@ def _assemble_html(responses, resource, querydict, url, extension):
|
||||
if iscollection:
|
||||
# localpath = url[:-2] (why was this here??)
|
||||
try:
|
||||
if url == '/users/':
|
||||
return
|
||||
firstpass = True
|
||||
for y in create_resource_functions[url]():
|
||||
module = url.split('/')
|
||||
if not module:
|
||||
return
|
||||
for y in create_resource_functions[module[-2]]():
|
||||
if firstpass:
|
||||
yield "<hr>Define new resource in %s:<BR>" % \
|
||||
url.split("/")[-2]
|
||||
yield "<hr>Define new resource in %s:<BR>" % module[-2]
|
||||
firstpass = False
|
||||
yield y
|
||||
yield ('<input value="create" name="restexplorerop" type="submit">'
|
||||
@@ -461,33 +676,37 @@ 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):
|
||||
haldata = rsp.raw()
|
||||
for hk in haldata.iterkeys():
|
||||
if 'href' in haldata[hk]:
|
||||
if isinstance(haldata[hk]['href'], int):
|
||||
haldata[hk]['href'] = str(haldata[hk]['href'])
|
||||
haldata[hk]['href'] += extension
|
||||
if hk in links:
|
||||
if isinstance(links[hk], list):
|
||||
links[hk].append(haldata[hk])
|
||||
else:
|
||||
links[hk] = [links[hk], haldata[hk]]
|
||||
elif hk == 'item':
|
||||
links[hk] = [haldata[hk],]
|
||||
else:
|
||||
links[hk] = haldata[hk]
|
||||
else:
|
||||
@@ -499,14 +718,20 @@ def _assemble_json(responses, resource, url, extension):
|
||||
else:
|
||||
rspdata[dk] = [rspdata[dk], rsp[dk]]
|
||||
else:
|
||||
rspdata[dk] = rsp[dk]
|
||||
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
|
||||
rspdata[dk] = [rsp[dk]]
|
||||
else:
|
||||
rspdata[dk] = rsp[dk]
|
||||
rspdata["_links"] = links
|
||||
tlvdata.unicode_dictvalues(rspdata)
|
||||
yield json.dumps(
|
||||
rspdata, sort_keys=True, indent=4, ensure_ascii=False).encode('utf-8')
|
||||
|
||||
|
||||
def serve():
|
||||
def serve(bind_host, bind_port):
|
||||
# TODO(jbjohnso): move to unix socket and explore
|
||||
# either making apache deal with it
|
||||
# or just supporting nginx or lighthttpd
|
||||
@@ -517,21 +742,34 @@ def serve():
|
||||
#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(('::', 4005, 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):
|
||||
def __init__(self):
|
||||
def __init__(self, bind_host=None, bind_port=None):
|
||||
self.server = None
|
||||
self.bind_host = bind_host or '::'
|
||||
self.bind_port = bind_port or 4005
|
||||
|
||||
def start(self):
|
||||
global auditlog
|
||||
global tracelog
|
||||
tracelog = log.Logger('trace')
|
||||
auditlog = log.Logger('audit')
|
||||
self.server = eventlet.spawn(serve)
|
||||
self.server = eventlet.spawn(serve, self.bind_host, self.bind_port)
|
||||
|
||||
|
||||
_cleaner = eventlet.spawn(_sessioncleaner)
|
||||
|
||||
@@ -28,7 +28,7 @@ class Console(object):
|
||||
"""This is the class defining the interface a console plugin must return
|
||||
for the _console/session element"""
|
||||
def __init__(self, node, config):
|
||||
raise NotImplementedError("Subclassing required")
|
||||
return
|
||||
|
||||
def connect(self, callback):
|
||||
raise NotImplementedError("Subclassing required")
|
||||
@@ -36,13 +36,10 @@ class Console(object):
|
||||
def write(self, data):
|
||||
raise NotImplementedError("Subclassing required")
|
||||
|
||||
def wait_for_data(self, timeout=600):
|
||||
raise NotImplementedError("Subclassing required")
|
||||
|
||||
def send_break(self):
|
||||
"""This function is how a plugin should implement sending a break to
|
||||
the remote console"""
|
||||
pass
|
||||
return
|
||||
|
||||
def ping(self):
|
||||
"""This function is a hint to the console plugin that now would be a
|
||||
@@ -54,4 +51,4 @@ class Console(object):
|
||||
as well, so consoles can schedule a health check to run at this time.
|
||||
No return is expected, any error condition can be reported by sending
|
||||
ConsoleEvent.Disconnect, just like normal."""
|
||||
pass
|
||||
return
|
||||
|
||||
@@ -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.
|
||||
@@ -61,14 +61,34 @@
|
||||
|
||||
import collections
|
||||
import confluent.config.configmanager
|
||||
import confluent.config.conf as conf
|
||||
import confluent.exceptions as exc
|
||||
import eventlet
|
||||
import fcntl
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import struct
|
||||
import time
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_SH
|
||||
except ImportError:
|
||||
if os.name == 'nt':
|
||||
import msvcrt
|
||||
LOCK_SH = msvcrt.LK_LOCK # no shared, degrade to exclusive
|
||||
LOCK_EX = msvcrt.LK_LOCK
|
||||
LOCK_UN = msvcrt.LK_UNLCK
|
||||
def flock(file, flag):
|
||||
oldoffset = file.tell()
|
||||
file.seek(0)
|
||||
msvcrt.locking(file.fileno(), flag, 1)
|
||||
file.seek(oldoffset)
|
||||
else:
|
||||
raise
|
||||
|
||||
# on conserving filehandles:
|
||||
# upon write, if file not open, open it for append
|
||||
# upon write, schedule/reschedule closing filehandle in 15 seconds
|
||||
@@ -80,14 +100,14 @@ import traceback
|
||||
# if that happens, warn to have user increase ulimit for optimal
|
||||
# performance
|
||||
|
||||
MIDNIGHT = 24 * 60 * 60
|
||||
_loggers = {}
|
||||
|
||||
|
||||
class Events(object):
|
||||
(
|
||||
undefined, clearscreen, clientconnect, clientdisconnect,
|
||||
consoledisconnect, consoleconnect, stacktrace
|
||||
) = range(7)
|
||||
consoledisconnect, consoleconnect, stacktrace, logrollover
|
||||
) = range(8)
|
||||
logstr = {
|
||||
2: 'connection by ',
|
||||
3: 'disconnection by ',
|
||||
@@ -98,6 +118,376 @@ class DataTypes(object):
|
||||
text, dictionary, console, event = range(4)
|
||||
|
||||
|
||||
class RollingTypes(object):
|
||||
no_rolling, size_rolling, time_rolling = range(3)
|
||||
|
||||
|
||||
class BaseRotatingHandler(object):
|
||||
|
||||
def __init__(self, filepath, logname):
|
||||
"""
|
||||
Use the specified filename for streamed logging
|
||||
"""
|
||||
self.filepath = filepath
|
||||
self.textpath = os.path.join(self.filepath, logname)
|
||||
self.binpath = os.path.join(self.filepath, logname + ".cbl")
|
||||
self.textfile = None
|
||||
self.binfile = None
|
||||
|
||||
def open(self):
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
self.textfile.seek(0, 2)
|
||||
if self.binfile is None:
|
||||
self.binfile = open(self.binpath, mode='ab')
|
||||
self.binfile.seek(0, 2)
|
||||
return self.textfile, self.binfile
|
||||
|
||||
def try_emit(self, binrecord, textrecord):
|
||||
"""
|
||||
Emit a record.
|
||||
|
||||
Output the record to the file, catering for rollover as described
|
||||
in doRollover().
|
||||
"""
|
||||
rolling_type = self.shouldRollover(binrecord, textrecord)
|
||||
if rolling_type:
|
||||
flock(self.textfile, LOCK_UN)
|
||||
return self.doRollover(rolling_type)
|
||||
return None
|
||||
|
||||
def emit(self, binrecord, textrecord):
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
if self.binfile is None:
|
||||
self.binfile = open(self.binpath, mode='ab')
|
||||
self.textfile.write(textrecord)
|
||||
self.binfile.write(binrecord)
|
||||
self.textfile.flush()
|
||||
self.binfile.flush()
|
||||
|
||||
def get_textfile_offset(self, data_len):
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
return self.textfile.tell() + data_len
|
||||
|
||||
def close(self):
|
||||
if self.textfile:
|
||||
if not self.textfile.closed:
|
||||
self.textfile.close()
|
||||
self.textfile = None
|
||||
if self.binfile:
|
||||
if not self.binfile.closed:
|
||||
self.binfile.close()
|
||||
self.binfile = None
|
||||
|
||||
|
||||
class TimedAndSizeRotatingFileHandler(BaseRotatingHandler):
|
||||
"""
|
||||
Handler for logging to a file, rotating the log file at certain timed
|
||||
intervals.
|
||||
|
||||
If backupCount is > 0, when rollover is done, no more than backupCount
|
||||
files are kept - the oldest ones are deleted.
|
||||
"""
|
||||
|
||||
def __init__(self, filepath, logname, interval=1):
|
||||
BaseRotatingHandler.__init__(self, filepath, logname)
|
||||
try:
|
||||
self.when = conf.get_option('log', 'when').upper()
|
||||
except (AttributeError):
|
||||
self.when = 'D'
|
||||
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:
|
||||
raise exc.GlobalConfigError("The minimum value of max_bytes "
|
||||
"of log rolling size in the log "
|
||||
"section should larger than 8192.")
|
||||
self.utc = conf.get_boolean_option('log', 'utc') or False
|
||||
|
||||
# Calculate the real rollover interval, which is just the number of
|
||||
# seconds between rollovers. Also set the filename suffix used when
|
||||
# a rollover occurs. Current 'when' events supported:
|
||||
# S - Seconds
|
||||
# M - Minutes
|
||||
# H - Hours
|
||||
# D - Days
|
||||
# midnight - roll over at midnight
|
||||
# W{0-6} - roll over on a certain day; 0 - Monday
|
||||
#
|
||||
# Case of the 'when' specifier is not important; lower or upper case
|
||||
# will work.
|
||||
if self.when == 'S':
|
||||
self.interval = 1 # one second
|
||||
self.suffix = "%Y-%m-%d_%H-%M-%S"
|
||||
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.{0,1}\d*$"
|
||||
elif self.when == 'M':
|
||||
self.interval = 60 # one minute
|
||||
self.suffix = "%Y-%m-%d_%H-%M"
|
||||
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}-\d{2}\.{0,1}\d*$"
|
||||
elif self.when == 'H':
|
||||
self.interval = 60 * 60 # one hour
|
||||
self.suffix = "%Y-%m-%d_%H"
|
||||
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}_\d{2}$"
|
||||
elif self.when == 'D' or self.when == 'MIDNIGHT':
|
||||
self.interval = 60 * 60 * 24 # one day
|
||||
self.suffix = "%Y-%m-%d"
|
||||
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}\.{0,1}\d*$"
|
||||
elif self.when.startswith('W'):
|
||||
self.interval = 60 * 60 * 24 * 7 # one week
|
||||
if len(self.when) != 2:
|
||||
raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
|
||||
if self.when[1] < '0' or self.when[1] > '6':
|
||||
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
|
||||
self.dayOfWeek = int(self.when[1])
|
||||
self.suffix = "%Y-%m-%d"
|
||||
self.extMatch = r"^(cbl\.){0,1}\d{4}-\d{2}-\d{2}\.{0,1}\d*$"
|
||||
else:
|
||||
raise ValueError("Invalid rollover interval specified: %s" % self.when)
|
||||
|
||||
self.extMatch = re.compile(self.extMatch)
|
||||
self.interval = self.interval * interval # multiply by units requested
|
||||
# Note: Get the modify time of text log file to calculate the
|
||||
# rollover time
|
||||
if os.path.exists(self.textpath):
|
||||
t = os.stat(self.textpath)[stat.ST_MTIME]
|
||||
else:
|
||||
t = int(time.time())
|
||||
self.rolloverAt = self.computeRollover(t)
|
||||
self.sizeRollingCount = 0
|
||||
self.initSizeRollingCount()
|
||||
|
||||
def computeRollover(self, currentTime):
|
||||
"""
|
||||
Work out the rollover time based on the specified time.
|
||||
"""
|
||||
result = currentTime + self.interval
|
||||
# If we are rolling over at midnight or weekly, then the interval is already known.
|
||||
# What we need to figure out is WHEN the next interval is. In other words,
|
||||
# if you are rolling over at midnight, then your base interval is 1 day,
|
||||
# but you want to start that one day clock at midnight, not now. So, we
|
||||
# have to fudge the rolloverAt value in order to trigger the first rollover
|
||||
# at the right time. After that, the regular interval will take care of
|
||||
# the rest. Note that this code doesn't care about leap seconds. :)
|
||||
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
|
||||
# This could be done with less code, but I wanted it to be clear
|
||||
if self.utc:
|
||||
t = time.gmtime(currentTime)
|
||||
else:
|
||||
t = time.localtime(currentTime)
|
||||
currentHour = t[3]
|
||||
currentMinute = t[4]
|
||||
currentSecond = t[5]
|
||||
# r is the number of seconds left between now and midnight
|
||||
r = MIDNIGHT - ((currentHour * 60 + currentMinute) * 60 +
|
||||
currentSecond)
|
||||
result = currentTime + r
|
||||
# If we are rolling over on a certain day, add in the number of days until
|
||||
# the next rollover, but offset by 1 since we just calculated the time
|
||||
# until the next day starts. There are three cases:
|
||||
# Case 1) The day to rollover is today; in this case, do nothing
|
||||
# Case 2) The day to rollover is further in the interval (i.e., today is
|
||||
# day 2 (Wednesday) and rollover is on day 6 (Sunday). Days to
|
||||
# next rollover is simply 6 - 2 - 1, or 3.
|
||||
# Case 3) The day to rollover is behind us in the interval (i.e., today
|
||||
# is day 5 (Saturday) and rollover is on day 3 (Thursday).
|
||||
# Days to rollover is 6 - 5 + 3, or 4. In this case, it's the
|
||||
# number of days left in the current week (1) plus the number
|
||||
# of days in the next week until the rollover day (3).
|
||||
# The calculations described in 2) and 3) above need to have a day added.
|
||||
# This is because the above time calculation takes us to midnight on this
|
||||
# day, i.e. the start of the next day.
|
||||
if self.when.startswith('W'):
|
||||
day = t[6] # 0 is Monday
|
||||
if day != self.dayOfWeek:
|
||||
if day < self.dayOfWeek:
|
||||
daysToWait = self.dayOfWeek - day
|
||||
else:
|
||||
daysToWait = 6 - day + self.dayOfWeek + 1
|
||||
newRolloverAt = result + (daysToWait * (60 * 60 * 24))
|
||||
if not self.utc:
|
||||
dstNow = t[-1]
|
||||
dstAtRollover = time.localtime(newRolloverAt)[-1]
|
||||
if dstNow != dstAtRollover:
|
||||
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
|
||||
addend = -3600
|
||||
else: # DST bows out before next rollover, so we need to add an hour
|
||||
addend = 3600
|
||||
newRolloverAt += addend
|
||||
result = newRolloverAt
|
||||
return result
|
||||
|
||||
def shouldRollover(self, binrecord, textrecord):
|
||||
"""
|
||||
Determine if rollover should occur.
|
||||
Just compare times.
|
||||
"""
|
||||
# time rolling first
|
||||
t = int(time.time())
|
||||
if t >= self.rolloverAt:
|
||||
return RollingTypes.time_rolling
|
||||
self.open()
|
||||
if self.maxBytes > 0: # are we rolling over?
|
||||
if self.textfile.tell() + len(textrecord) >= self.maxBytes:
|
||||
return RollingTypes.size_rolling
|
||||
if self.binfile.tell() + len(binrecord) >= self.maxBytes:
|
||||
return RollingTypes.size_rolling
|
||||
return RollingTypes.no_rolling
|
||||
|
||||
def getFilesToDelete(self):
|
||||
"""
|
||||
Determine the files to delete when rolling over.
|
||||
"""
|
||||
dirName, baseName = os.path.split(self.textpath)
|
||||
files = []
|
||||
prefix = baseName + "."
|
||||
filePaths = glob.glob(os.path.join(dirName, "%s*" % prefix))
|
||||
fileNames = [os.path.split(f)[1] for f in filePaths]
|
||||
plen = len(prefix)
|
||||
t_set = set()
|
||||
for fileName in fileNames:
|
||||
suffix = fileName[plen:]
|
||||
if self.extMatch.match(suffix):
|
||||
s = suffix.split(".")
|
||||
t = s[1] if suffix.startswith("cbl") else s[0]
|
||||
t_set.add(t)
|
||||
files.append({'time': t, 'file': os.path.join(dirName,
|
||||
fileName)})
|
||||
|
||||
t_list = list(t_set)
|
||||
t_list.sort()
|
||||
result = [f['file'] for f in files if
|
||||
f['time'] in t_list[:-(self.backupCount - 1)]]
|
||||
return result
|
||||
|
||||
def initSizeRollingCount(self):
|
||||
"""
|
||||
Init the max number of log files for current time.
|
||||
"""
|
||||
dirName, baseName = os.path.split(self.textpath)
|
||||
prefix = baseName + "."
|
||||
filePaths = glob.glob(os.path.join(dirName, "%s*" % prefix))
|
||||
fileNames = [os.path.split(f)[1] for f in filePaths]
|
||||
plen = len(prefix)
|
||||
for fileName in fileNames:
|
||||
suffix = fileName[plen:]
|
||||
try:
|
||||
self.sizeRollingCount = max(self.sizeRollingCount, int(suffix))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def _sizeRoll(self):
|
||||
self.close()
|
||||
for i in range(self.sizeRollingCount, 0, -1):
|
||||
sbfn = "%s.%d" % (self.binpath, i)
|
||||
dbfn = "%s.%d" % (self.binpath, i + 1)
|
||||
stfn = "%s.%d" % (self.textpath, i)
|
||||
dtfn = "%s.%d" % (self.textpath, i + 1)
|
||||
if os.path.exists(sbfn):
|
||||
if os.path.exists(dbfn):
|
||||
os.remove(dbfn)
|
||||
os.rename(sbfn, dbfn)
|
||||
if os.path.exists(stfn):
|
||||
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):
|
||||
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):
|
||||
os.rename(self.textpath, dtfn)
|
||||
return dbfn, dtfn
|
||||
|
||||
def _timeRoll(self):
|
||||
self.close()
|
||||
# get the time that this sequence started at and make it a TimeTuple
|
||||
currentTime = int(time.time())
|
||||
dstNow = time.localtime(currentTime)[-1]
|
||||
t = self.rolloverAt - self.interval
|
||||
if self.utc:
|
||||
timeTuple = time.gmtime(t)
|
||||
else:
|
||||
timeTuple = time.localtime(t)
|
||||
dstThen = timeTuple[-1]
|
||||
if dstNow != dstThen:
|
||||
if dstNow:
|
||||
addend = 3600
|
||||
else:
|
||||
addend = -3600
|
||||
timeTuple = time.localtime(t + addend)
|
||||
|
||||
# if size rolling files exist
|
||||
for i in range(self.sizeRollingCount, 0, -1):
|
||||
sbfn = "%s.%d" % ( self.binpath, i)
|
||||
dbfn = "%s.%s.%d" % (
|
||||
self.binpath, time.strftime(self.suffix, timeTuple),i)
|
||||
stfn = "%s.%d" % (self.textpath, i)
|
||||
dtfn = "%s.%s.%d" % (
|
||||
self.textpath, time.strftime(self.suffix, timeTuple), i)
|
||||
if os.path.exists(sbfn):
|
||||
if os.path.exists(dbfn):
|
||||
os.remove(dbfn)
|
||||
os.rename(sbfn, dbfn)
|
||||
if os.path.exists(stfn):
|
||||
if os.path.exists(dtfn):
|
||||
os.remove(dtfn)
|
||||
os.rename(stfn, dtfn)
|
||||
|
||||
# As time rolling happens, reset statistics count
|
||||
self.sizeRollingCount = 0
|
||||
dbfn = self.binpath + "." + time.strftime(self.suffix, timeTuple)
|
||||
odbfn = dbfn
|
||||
dtfn = self.textpath + "." + time.strftime(self.suffix, timeTuple)
|
||||
odtfn = dtfn
|
||||
append=1
|
||||
while os.path.exists(dbfn):
|
||||
dbfn = odbfn + '.{}'.format(append)
|
||||
append += 1
|
||||
append=1
|
||||
while os.path.exists(dtfn):
|
||||
dtfn = odtfn + '.{}'.format(append)
|
||||
append += 1
|
||||
if os.path.exists(self.binpath):
|
||||
os.rename(self.binpath, dbfn)
|
||||
if os.path.exists(self.textpath):
|
||||
os.rename(self.textpath, dtfn)
|
||||
if self.backupCount > 0:
|
||||
for s in self.getFilesToDelete():
|
||||
os.remove(s)
|
||||
|
||||
newRolloverAt = self.computeRollover(currentTime)
|
||||
while newRolloverAt <= currentTime:
|
||||
newRolloverAt = newRolloverAt + self.interval
|
||||
#If DST changes and midnight or weekly rollover, adjust for this.
|
||||
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
|
||||
dstAtRollover = time.localtime(newRolloverAt)[-1]
|
||||
if dstNow != dstAtRollover:
|
||||
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
|
||||
addend = -3600
|
||||
else: # DST bows out before next rollover, so we need to add an hour
|
||||
addend = 3600
|
||||
newRolloverAt += addend
|
||||
self.rolloverAt = newRolloverAt
|
||||
return dbfn, dtfn
|
||||
|
||||
def doRollover(self, rolling_type):
|
||||
"""
|
||||
do a rollover based on the rolling type.
|
||||
"""
|
||||
if rolling_type == RollingTypes.size_rolling:
|
||||
return self._sizeRoll()
|
||||
if rolling_type == RollingTypes.time_rolling:
|
||||
return self._timeRoll()
|
||||
|
||||
|
||||
class Logger(object):
|
||||
"""
|
||||
:param console: If true, [] will be used to denote non-text events. If
|
||||
@@ -122,26 +512,28 @@ class Logger(object):
|
||||
self.initialized = True
|
||||
self.filepath = confluent.config.configmanager.get_global("logdirectory")
|
||||
if self.filepath is None:
|
||||
self.filepath = "/var/log/confluent/"
|
||||
if os.name == 'nt':
|
||||
self.filepath = os.path.join(
|
||||
os.getenv('SystemDrive'), '\\ProgramData', 'confluent',
|
||||
'logs')
|
||||
else:
|
||||
self.filepath = "/var/log/confluent"
|
||||
self.isconsole = console
|
||||
if console:
|
||||
self.filepath += "consoles/"
|
||||
self.filepath = os.path.join(self.filepath, "consoles")
|
||||
if not os.path.isdir(self.filepath):
|
||||
os.makedirs(self.filepath, 448)
|
||||
self.textpath = self.filepath + logname
|
||||
self.binpath = self.filepath + logname + ".cbl"
|
||||
self.writer = None
|
||||
self.closer = None
|
||||
self.textfile = None
|
||||
self.binfile = None
|
||||
self.handler = TimedAndSizeRotatingFileHandler(self.filepath, logname,
|
||||
interval=1)
|
||||
self.lockfile = None
|
||||
self.logname = logname
|
||||
self.logentries = collections.deque()
|
||||
|
||||
def writedata(self):
|
||||
if self.textfile is None:
|
||||
self.textfile = open(self.textpath, mode='ab')
|
||||
if self.binfile is None:
|
||||
self.binfile = open(self.binpath, mode='ab')
|
||||
while self.logentries:
|
||||
textfile, binfile = self.handler.open()
|
||||
entry = self.logentries.popleft()
|
||||
ltype = entry[0]
|
||||
tstamp = entry[1]
|
||||
@@ -156,8 +548,8 @@ class Logger(object):
|
||||
elif not self.isconsole:
|
||||
textdate = time.strftime(
|
||||
'%b %d %H:%M:%S ', time.localtime(tstamp))
|
||||
fcntl.flock(self.textfile, fcntl.LOCK_EX)
|
||||
offset = self.textfile.tell() + len(textdate)
|
||||
flock(textfile, LOCK_EX)
|
||||
offset = textfile.tell() + len(textdate)
|
||||
datalen = len(data)
|
||||
eventaux = entry[4]
|
||||
if eventaux is None:
|
||||
@@ -175,54 +567,123 @@ class Logger(object):
|
||||
textrecord = textdate + data
|
||||
if not textrecord.endswith('\n'):
|
||||
textrecord += '\n'
|
||||
self.textfile.write(textrecord)
|
||||
fcntl.flock(self.textfile, fcntl.LOCK_UN)
|
||||
fcntl.flock(self.binfile, fcntl.LOCK_EX)
|
||||
self.binfile.write(binrecord)
|
||||
fcntl.flock(self.binfile, fcntl.LOCK_UN)
|
||||
self.textfile.flush()
|
||||
self.binfile.flush()
|
||||
files = self.handler.try_emit(binrecord, textrecord)
|
||||
if not files:
|
||||
self.handler.emit(binrecord, textrecord)
|
||||
flock(textfile, LOCK_UN)
|
||||
else:
|
||||
# Log the rolling event at first, then log the last data
|
||||
# which cause the rolling event.
|
||||
to_bfile, to_tfile = files
|
||||
self.logentries.appendleft(entry)
|
||||
roll_data = json.dumps({'previouslogfile': to_tfile})
|
||||
self.logentries.appendleft([DataTypes.event, tstamp, roll_data,
|
||||
Events.logrollover, None])
|
||||
if self.closer is None:
|
||||
self.closer = eventlet.spawn_after(15, self.closelog)
|
||||
self.writer = None
|
||||
|
||||
def read_recent_text(self, size):
|
||||
|
||||
def parse_last_rolling_files(textfile, offset, datalen):
|
||||
textfile.seek(offset, 0)
|
||||
textpath = json.loads(textfile.read(datalen))['previouslogfile']
|
||||
dir_name, base_name = os.path.split(textpath)
|
||||
temp = base_name.split('.')
|
||||
temp.insert(1,'cbl')
|
||||
# find the recent bin file
|
||||
binpath = os.path.join(dir_name, ".".join(temp))
|
||||
return textpath, binpath
|
||||
|
||||
textpath = self.handler.textpath
|
||||
binpath = self.handler.binpath
|
||||
try:
|
||||
textfile = open(self.textpath, mode='r')
|
||||
binfile = open(self.binpath, mode='r')
|
||||
textfile = open(textpath, mode='r')
|
||||
binfile = open(binpath, mode='r')
|
||||
except IOError:
|
||||
return '', 0, 0
|
||||
fcntl.flock(binfile, fcntl.LOCK_SH)
|
||||
flock(binfile, LOCK_SH)
|
||||
binfile.seek(0, 2)
|
||||
binidx = binfile.tell() - 16
|
||||
binidx = binfile.tell()
|
||||
currsize = 0
|
||||
offsets = []
|
||||
termstate = None
|
||||
recenttimestamp = 0
|
||||
flock(textfile, LOCK_SH)
|
||||
while binidx > 0 and currsize < size:
|
||||
binfile.seek(binidx, 0)
|
||||
binidx -= 16
|
||||
binfile.seek(binidx, 0)
|
||||
recbytes = binfile.read(16)
|
||||
(_, ltype, offset, datalen, tstamp, evtdata, eventaux, _) = \
|
||||
struct.unpack(">BBIHIBBH", recbytes)
|
||||
if ltype != 2:
|
||||
# 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
|
||||
# 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:
|
||||
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
|
||||
# previous log file, go to next iteration
|
||||
continue
|
||||
elif ltype != 2:
|
||||
continue
|
||||
if tstamp > recenttimestamp:
|
||||
recenttimestamp = tstamp
|
||||
currsize += datalen
|
||||
offsets.append((offset, datalen))
|
||||
offsets.append((offset, datalen, textpath))
|
||||
if termstate is None:
|
||||
termstate = eventaux
|
||||
fcntl.flock(binfile, fcntl.LOCK_UN)
|
||||
binfile.close()
|
||||
try:
|
||||
flock(binfile, LOCK_UN)
|
||||
binfile.close()
|
||||
except:
|
||||
pass
|
||||
textdata = ''
|
||||
fcntl.flock(textfile, fcntl.LOCK_SH)
|
||||
while offsets:
|
||||
(offset, length) = offsets.pop()
|
||||
textfile.seek(offset, 0)
|
||||
(offset, length, textpath) = offsets.pop()
|
||||
if textfile is None:
|
||||
textfile = open(textpath, mode='r')
|
||||
flock(textfile, LOCK_SH)
|
||||
if textfile.name != textpath:
|
||||
try:
|
||||
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)
|
||||
fcntl.flock(textfile, fcntl.LOCK_UN)
|
||||
textfile.close()
|
||||
try:
|
||||
flock(textfile, LOCK_UN)
|
||||
textfile.close()
|
||||
except:
|
||||
pass
|
||||
if termstate is None:
|
||||
termstate = 0
|
||||
return textdata, termstate, recenttimestamp
|
||||
@@ -270,8 +731,21 @@ class Logger(object):
|
||||
self.writer = eventlet.spawn_after(2, self.writedata)
|
||||
|
||||
def closelog(self):
|
||||
self.textfile.close()
|
||||
self.binfile.close()
|
||||
self.textfile = None
|
||||
self.binfile = None
|
||||
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)
|
||||
@@ -0,0 +1,74 @@
|
||||
# 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.
|
||||
|
||||
# Utility library for interesting lookups of nodes.
|
||||
# Examples:
|
||||
# looking up a node by a hardwaremanagement.manager address
|
||||
# looking up a node by uuid (actually pretty straightforward
|
||||
# looking up a node by mac address
|
||||
# These are generally in the context of coming in from some unstructured
|
||||
# direction (alerts, PXE attempt) and for now will only look at the null
|
||||
# tenant (all baremetal tenants that are expected to receive alert/pxe
|
||||
# service should have a null tenant and a tenant entry that correlates)
|
||||
__author__ = 'jjohnson2'
|
||||
|
||||
import confluent.config.configmanager as configmanager
|
||||
import itertools
|
||||
from eventlet.support import greendns
|
||||
|
||||
manager_to_nodemap = {}
|
||||
|
||||
def node_by_manager(manager):
|
||||
"""Lookup a node by manager
|
||||
|
||||
Search for a node according to a given network address.
|
||||
Rather than do a simple equality, it uses getaddrinfo
|
||||
to allow name or ip and different forms of ip. For
|
||||
example, 'fe80::0001' will match 'fe80::01' and
|
||||
'127.000.000.001' will match '127.0.0.1'
|
||||
|
||||
:param manager: The ip or resolvable name of the manager
|
||||
|
||||
:returns: The node name (if any)
|
||||
"""
|
||||
|
||||
manageraddresses = []
|
||||
for tmpaddr in greendns.getaddrinfo(manager, None):
|
||||
manageraddresses.append(tmpaddr[4][0])
|
||||
cfm = configmanager.ConfigManager(None)
|
||||
if manager in manager_to_nodemap:
|
||||
# We have a stored hint as to the most probably correct answer
|
||||
# put that node at the head of the list in hopes of reducing
|
||||
# iterations for a lookup in a large environment
|
||||
# However we don't trust the answer either, since
|
||||
# reconfiguration could have changed it and this mapping
|
||||
# is not hooked into getting updates
|
||||
check_nodes = itertools.chain(
|
||||
(manager_to_nodemap[manager],), cfm.list_nodes())
|
||||
else:
|
||||
check_nodes = cfm.list_nodes()
|
||||
hmattribs = cfm.get_node_attributes(check_nodes,
|
||||
('hardwaremanagement.manager',))
|
||||
for node in hmattribs:
|
||||
currhm = hmattribs[node]['hardwaremanagement.manager']['value']
|
||||
if currhm in manageraddresses:
|
||||
manager_to_nodemap[manager] = node
|
||||
return node
|
||||
for curraddr in greendns.getaddrinfo(currhm, None):
|
||||
curraddr = curraddr[4][0]
|
||||
if curraddr in manageraddresses:
|
||||
manager_to_nodemap[manager] = node
|
||||
return node
|
||||
@@ -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.
|
||||
@@ -27,21 +27,42 @@
|
||||
|
||||
import atexit
|
||||
import confluent.auth as auth
|
||||
import confluent.config.conf as conf
|
||||
import confluent.config.configmanager as configmanager
|
||||
import confluent.consoleserver as consoleserver
|
||||
import confluent.core as confluentcore
|
||||
import confluent.httpapi as httpapi
|
||||
import confluent.log as log
|
||||
import confluent.sockapi as sockapi
|
||||
try:
|
||||
import confluent.sockapi as sockapi
|
||||
except ImportError:
|
||||
#On platforms without pwd, give up on the sockapi in general and be http
|
||||
#only for now
|
||||
pass
|
||||
import eventlet
|
||||
#import eventlet.backdoor as backdoor
|
||||
import fcntl
|
||||
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
|
||||
|
||||
|
||||
def _daemonize():
|
||||
if not 'fork' in os.__dict__:
|
||||
return
|
||||
thispid = os.fork()
|
||||
if thispid > 0:
|
||||
os.waitpid(thispid, 0)
|
||||
@@ -74,9 +95,15 @@ def _checkpidfile():
|
||||
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()
|
||||
@@ -106,32 +133,104 @@ def _checkpidfile():
|
||||
def terminate(signalname, frame):
|
||||
sys.exit(0)
|
||||
|
||||
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:\n')
|
||||
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()):
|
||||
os.remove('/var/run/confluent/pid')
|
||||
|
||||
|
||||
def _initsecurity(config):
|
||||
if config.has_option('security', 'externalcfgkey'):
|
||||
keyfile = config.get('security', 'externalcfgkey')
|
||||
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():
|
||||
_checkpidfile()
|
||||
confluentcore.load_plugins()
|
||||
setlimits()
|
||||
signal.signal(signal.SIGUSR1, dumptrace)
|
||||
if havefcntl:
|
||||
_checkpidfile()
|
||||
conf.init_config()
|
||||
try:
|
||||
config = conf.get_config()
|
||||
_initsecurity(config)
|
||||
except:
|
||||
sys.stderr.write("Error unlocking credential store\n")
|
||||
doexit()
|
||||
sys.exit(1)
|
||||
try:
|
||||
confluentcore.load_plugins()
|
||||
except:
|
||||
doexit()
|
||||
raise
|
||||
_daemonize()
|
||||
_updatepidfile()
|
||||
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)
|
||||
consoleserver.start_console_sessions()
|
||||
webservice = httpapi.HttpApi()
|
||||
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)
|
||||
os.umask(oumask)
|
||||
http_bind_host, http_bind_port = _get_connector_config('http')
|
||||
sock_bind_host, sock_bind_port = _get_connector_config('socket')
|
||||
webservice = httpapi.HttpApi(http_bind_host, http_bind_port)
|
||||
webservice.start()
|
||||
sockservice = sockapi.SockApi()
|
||||
sockservice.start()
|
||||
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)
|
||||
|
||||
def _get_connector_config(session):
|
||||
host = conf.get_option(session, 'bindhost')
|
||||
port = conf.get_int_option(session, 'bindport')
|
||||
return (host, port)
|
||||
|
||||
@@ -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.
|
||||
@@ -19,8 +19,17 @@
|
||||
# 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
|
||||
from copy import deepcopy
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
valid_health_values = set([
|
||||
'ok',
|
||||
'warning',
|
||||
'critical',
|
||||
'failed',
|
||||
'unknown',
|
||||
])
|
||||
|
||||
def _htmlify_structure(indict):
|
||||
ret = "<ul>"
|
||||
@@ -29,12 +38,20 @@ def _htmlify_structure(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):
|
||||
if len(indict) > 0:
|
||||
if type(indict[0]) in (str, unicode):
|
||||
ret += ",".join(indict)
|
||||
if type(indict[0]) in (str, unicode, None):
|
||||
nd = []
|
||||
for datum in indict:
|
||||
if datum is None:
|
||||
nd.append('')
|
||||
else:
|
||||
nd.append(datum)
|
||||
ret += ",".join(nd)
|
||||
else:
|
||||
for v in indict:
|
||||
ret += _htmlify_structure(v)
|
||||
@@ -42,6 +59,7 @@ def _htmlify_structure(indict):
|
||||
|
||||
|
||||
class ConfluentMessage(object):
|
||||
apicode = 200
|
||||
readonly = False
|
||||
defaultvalue = ''
|
||||
defaulttype = 'text'
|
||||
@@ -65,7 +83,8 @@ class ConfluentMessage(object):
|
||||
"""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}
|
||||
|
||||
@@ -75,7 +94,7 @@ class ConfluentMessage(object):
|
||||
self.kvpairs = self.kvpairs[node]
|
||||
|
||||
def html(self, extension=''):
|
||||
#this is used to facilitate the api explorer feature
|
||||
# this is used to facilitate the api explorer feature
|
||||
if not hasattr(self, 'stripped'):
|
||||
self.stripped = False
|
||||
if not hasattr(self, 'notnode'):
|
||||
@@ -94,8 +113,31 @@ class ConfluentMessage(object):
|
||||
for key in pairs.iterkeys():
|
||||
val = pairs[key]
|
||||
value = self.defaultvalue
|
||||
valtype = self.defaulttype
|
||||
if isinstance(val, dict) and 'type' in val:
|
||||
valtype = val['type']
|
||||
else:
|
||||
valtype = self.defaulttype
|
||||
notes = []
|
||||
|
||||
if isinstance(val, list):
|
||||
snippet += key + ":"
|
||||
if len(val) == 0 and not self.readonly:
|
||||
snippet += ('<input type="{0}" name="{1}" value="" '
|
||||
' "title="{2}">'
|
||||
).format(valtype, key, self.desc)
|
||||
for v in val:
|
||||
if self.readonly:
|
||||
snippet += _htmlify_structure(v)
|
||||
else:
|
||||
snippet += ('<input type="{0}" name="{1}" value="{2}" '
|
||||
' "title="{3}">\r'
|
||||
).format(valtype, key, v, self.desc)
|
||||
if not self.readonly:
|
||||
snippet += (
|
||||
'<input type="{0}" name="{1}" value="" title="{2}">'
|
||||
'<input type="checkbox" name="restexplorerhonorkey" '
|
||||
'value="{1}">\r').format(valtype, key, self.desc)
|
||||
return snippet
|
||||
if val is not None and 'value' in val:
|
||||
value = val['value']
|
||||
if 'inheritedfrom' in val:
|
||||
@@ -119,32 +161,13 @@ class ConfluentMessage(object):
|
||||
if 'inheritedfrom' in val:
|
||||
notes.append('Inherited from %s' % val['inheritedfrom'])
|
||||
value = '********'
|
||||
if isinstance(val, list):
|
||||
snippet += key + ":"
|
||||
if len(val) == 0 and not self.readonly:
|
||||
snippet += ('<input type="{0}" name="{1}" value="" '
|
||||
' "title="{2}">'
|
||||
).format(valtype, key, self.desc)
|
||||
for v in val:
|
||||
if self.readonly:
|
||||
snippet += _htmlify_structure(v)
|
||||
else:
|
||||
snippet += ('<input type="{0}" name="{1}" value="{2}" '
|
||||
' "title="{3}">'
|
||||
).format(valtype, key, v, self.desc)
|
||||
if not self.readonly:
|
||||
snippet += (
|
||||
'<input type="{0}" name="{1}" value="" title="{2}">'
|
||||
'<input type="checkbox" name="restexplorerhonorkey" '
|
||||
'value="{1}">').format(valtype, key, self.desc)
|
||||
return snippet
|
||||
if self.readonly:
|
||||
snippet += "{0}: {1}".format(key, value)
|
||||
else:
|
||||
snippet += (key + ":" +
|
||||
'<input type="{0}" name="{1}" value="{2}" '
|
||||
'title="{3}"><input type="checkbox" '
|
||||
'name="restexplorerhonorkey" value="{1}">'
|
||||
'name="restexplorerhonorkey" value="{1}"><br>\r'
|
||||
).format(valtype, key, value, self.desc)
|
||||
if len(notes) > 0:
|
||||
snippet += '(' + ','.join(notes) + ')'
|
||||
@@ -152,32 +175,40 @@ 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
|
||||
|
||||
def strip_node(self, node):
|
||||
#NOTE(jbjohnso): For single node errors, raise exception to
|
||||
#trigger what a developer of that medium would expect
|
||||
# NOTE(jjohnson2): For single node errors, raise exception to
|
||||
# trigger what a developer of that medium would expect
|
||||
raise Exception(self.error)
|
||||
|
||||
|
||||
class ConfluentTargetTimeout(ConfluentNodeError):
|
||||
def __init__(self, node):
|
||||
apicode = 504
|
||||
|
||||
def __init__(self, node, errstr='timeout'):
|
||||
self.node = node
|
||||
self.error = 'timeout'
|
||||
self.error = errstr
|
||||
|
||||
|
||||
def strip_node(self, node):
|
||||
raise exc.TargetEndpointUnreachable
|
||||
raise exc.TargetEndpointUnreachable(self.error)
|
||||
|
||||
|
||||
class ConfluentTargetNotFound(ConfluentNodeError):
|
||||
apicode = 404
|
||||
|
||||
def __init__(self, node, errorstr='not found'):
|
||||
self.node = node
|
||||
self.error = errorstr
|
||||
@@ -185,7 +216,9 @@ class ConfluentTargetNotFound(ConfluentNodeError):
|
||||
def strip_node(self, node):
|
||||
raise exc.NotFoundException(self.error)
|
||||
|
||||
|
||||
class ConfluentTargetInvalidCredentials(ConfluentNodeError):
|
||||
apicode = 502
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.error = 'bad credentials'
|
||||
@@ -248,7 +281,6 @@ class LinkRelation(ConfluentMessage):
|
||||
self.href = ''
|
||||
self.rel = ''
|
||||
|
||||
|
||||
def json(self):
|
||||
"""Provide json_hal style representation of the relation.
|
||||
|
||||
@@ -293,19 +325,68 @@ class ChildCollection(LinkRelation):
|
||||
extension)
|
||||
|
||||
|
||||
def get_input_message(path, operation, inputdata, nodes=None):
|
||||
def get_input_message(path, operation, inputdata, nodes=None, multinode=False):
|
||||
if path[0] == 'power' and path[1] == 'state' and operation != 'retrieve':
|
||||
return InputPowerMessage(path, nodes, inputdata)
|
||||
elif path[0] in ('attributes', 'users') and operation != 'retrieve':
|
||||
return InputAttributes(path, inputdata, nodes)
|
||||
elif path == ['boot', 'nextdevice'] and operation != 'retrieve':
|
||||
return InputBootDevice(path, nodes, inputdata)
|
||||
elif path == [ 'identify' ] and operation != 'retrieve':
|
||||
elif (len(path) == 5 and
|
||||
path[:4] == ['configuration', 'management_controller', 'alerts',
|
||||
'destinations'] and operation != 'retrieve'):
|
||||
return InputAlertDestination(path, nodes, inputdata, multinode)
|
||||
elif path == ['identify'] and operation != 'retrieve':
|
||||
return InputIdentifyMessage(path, nodes, inputdata)
|
||||
elif path == ['events', 'hardware', 'decode']:
|
||||
return InputAlertData(path, inputdata, nodes)
|
||||
elif (path[:3] == ['configuration', 'management_controller', 'users'] and
|
||||
operation not in ('retrieve', 'delete') and path[-1] != 'all'):
|
||||
return InputCredential(path, inputdata, nodes)
|
||||
elif (path[:3] == ['configuration', 'management_controller', 'reset']
|
||||
and operation != 'retrieve'):
|
||||
return InputBMCReset(path, nodes, inputdata)
|
||||
elif (path[:3] == ['configuration', 'management_controller', 'identifier']
|
||||
and operation != 'retrieve'):
|
||||
return InputMCI(path, nodes, inputdata)
|
||||
elif (path[:4] == ['configuration', 'management_controller',
|
||||
'net_interfaces', 'management'] and operation != 'retrieve'):
|
||||
return InputNetworkConfiguration(path, nodes, inputdata)
|
||||
elif (path[:3] == ['configuration', 'management_controller', 'domain_name']
|
||||
and operation != 'retrieve'):
|
||||
return InputDomainName(path, nodes, inputdata)
|
||||
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
|
||||
'enabled'] and operation != 'retrieve'):
|
||||
return InputNTPEnabled(path, nodes, inputdata)
|
||||
elif (path[:4] == ['configuration', 'management_controller', 'ntp',
|
||||
'servers'] and operation != 'retrieve' and len(path) == 5):
|
||||
return InputNTPServer(path, nodes, inputdata)
|
||||
elif inputdata:
|
||||
raise exc.InvalidArgumentException()
|
||||
|
||||
|
||||
class InputAlertData(ConfluentMessage):
|
||||
|
||||
def __init__(self, path, inputdata, nodes=None):
|
||||
self.alertparams = inputdata
|
||||
# first migrate snmpv1 input to snmpv2 format
|
||||
if 'specifictrap' in self.alertparams:
|
||||
# If we have a 'specifictrap', convert to SNMPv2 per RFC 2576
|
||||
# This way
|
||||
enterprise = self.alertparams['enterprise']
|
||||
specifictrap = self.alertparams['specifictrap']
|
||||
self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = enterprise + '.0.' + \
|
||||
str(specifictrap)
|
||||
if '1.3.6.1.6.3.1.1.4.1.0' in self.alertparams:
|
||||
self.alertparams['.1.3.6.1.6.3.1.1.4.1.0'] = \
|
||||
self.alertparams['1.3.6.1.6.3.1.1.4.1.0']
|
||||
if '.1.3.6.1.6.3.1.1.4.1.0' not in self.alertparams:
|
||||
raise exc.InvalidArgumentException('Missing SNMP Trap OID')
|
||||
|
||||
def get_alert(self, node=None):
|
||||
return self.alertparams
|
||||
|
||||
|
||||
class InputAttributes(ConfluentMessage):
|
||||
def __init__(self, path, inputdata, nodes=None):
|
||||
self.nodeattribs = {}
|
||||
@@ -348,14 +429,14 @@ class InputAttributes(ConfluentMessage):
|
||||
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:
|
||||
# as above, use format() to see if string follows
|
||||
# expression, store value back in case of escapes
|
||||
tv = nodeattr[attr].format()
|
||||
nodeattr[attr] = tv
|
||||
nodeattr[attr] = str(tv)
|
||||
except (KeyError, IndexError):
|
||||
# an expression string will error if format() done
|
||||
# use that as cue to put it into config as an expr
|
||||
@@ -363,6 +444,80 @@ class InputAttributes(ConfluentMessage):
|
||||
return nodeattr
|
||||
|
||||
|
||||
class InputCredential(ConfluentMessage):
|
||||
valid_privilege_levels = set([
|
||||
'callback',
|
||||
'user',
|
||||
'operator',
|
||||
'administrator',
|
||||
'proprietary',
|
||||
'no_access',
|
||||
])
|
||||
|
||||
valid_enabled_values = set([
|
||||
'yes',
|
||||
'no'
|
||||
])
|
||||
|
||||
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')
|
||||
if (isinstance(inputdata['uid'], str) and
|
||||
not inputdata['uid'].isdigit()):
|
||||
raise exc.InvalidArgumentException('uid must be a number')
|
||||
else:
|
||||
inputdata['uid'] = int(inputdata['uid'])
|
||||
if ('privilege_level' in inputdata and
|
||||
inputdata['privilege_level'] not in self.valid_privilege_levels):
|
||||
raise exc.InvalidArgumentException('privilege_level is not one of '
|
||||
+ ','.join(self.valid_privilege_levels))
|
||||
if 'username' in inputdata and len(inputdata['username']) > 16:
|
||||
raise exc.InvalidArgumentException(
|
||||
'name must be less than or = 16 chars')
|
||||
if 'password' in inputdata and len(inputdata['password']) > 20:
|
||||
raise exc.InvalidArgumentException('password has limit of 20 chars')
|
||||
|
||||
if ('enabled' in inputdata and
|
||||
inputdata['enabled'] not in self.valid_enabled_values):
|
||||
raise exc.InvalidArgumentException('valid values for enabled are '
|
||||
+ 'yes and no')
|
||||
|
||||
if nodes is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'This only supports per-node input')
|
||||
for node in nodes:
|
||||
self.credentials[node] = inputdata
|
||||
|
||||
def get_attributes(self, node):
|
||||
if node not in self.credentials:
|
||||
return {}
|
||||
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
|
||||
tv = credential[attr].format()
|
||||
credential[attr] = tv
|
||||
except (KeyError, IndexError):
|
||||
# an expression string will error if format() done
|
||||
# use that as cue to put it into config as an expr
|
||||
credential[attr] = {'expression': credential[attr]}
|
||||
return credential
|
||||
|
||||
|
||||
class ConfluentInputMessage(ConfluentMessage):
|
||||
keyname = 'state'
|
||||
|
||||
@@ -372,7 +527,7 @@ class ConfluentInputMessage(ConfluentMessage):
|
||||
if not inputdata:
|
||||
raise exc.InvalidArgumentException('missing input data')
|
||||
if self.keyname not in inputdata:
|
||||
#assume we have nested information
|
||||
# assume we have nested information
|
||||
for key in nodes:
|
||||
if key not in inputdata:
|
||||
raise exc.InvalidArgumentException(key + ' not in request')
|
||||
@@ -388,7 +543,8 @@ class ConfluentInputMessage(ConfluentMessage):
|
||||
else: # we have a state argument not by node
|
||||
datum = inputdata
|
||||
if self.keyname not in datum:
|
||||
raise exc.InvalidArgumentException('missing {0} argument'.format(self.keyname))
|
||||
raise exc.InvalidArgumentException(
|
||||
'missing {0} argument'.format(self.keyname))
|
||||
elif datum[self.keyname] not in self.valid_values:
|
||||
raise exc.InvalidArgumentException(datum[self.keyname] +
|
||||
' is not one of ' +
|
||||
@@ -412,12 +568,125 @@ class InputPowerMessage(ConfluentInputMessage):
|
||||
'off',
|
||||
'reset',
|
||||
'boot',
|
||||
'diag',
|
||||
'shutdown',
|
||||
])
|
||||
|
||||
def powerstate(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class InputBMCReset(ConfluentInputMessage):
|
||||
valid_values = set([
|
||||
'reset',
|
||||
])
|
||||
|
||||
def state(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class InputMCI(ConfluentInputMessage):
|
||||
def __init__(self, path, nodes, inputdata):
|
||||
self.inputbynode = {}
|
||||
self.stripped = False
|
||||
if not inputdata or 'identifier' not in inputdata:
|
||||
raise exc.InvalidArgumentException('missing input data')
|
||||
if len(inputdata['identifier']) > 64:
|
||||
raise exc.InvalidArgumentException(
|
||||
'identifier must be less than or = 64 chars')
|
||||
|
||||
if nodes is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'This only supports per-node input')
|
||||
for node in nodes:
|
||||
self.inputbynode[node] = inputdata
|
||||
|
||||
def mci(self, node):
|
||||
return self.inputbynode[node]['identifier']
|
||||
|
||||
|
||||
class InputNetworkConfiguration(ConfluentInputMessage):
|
||||
def __init__(self, path, nodes, inputdata):
|
||||
self.inputbynode = {}
|
||||
self.stripped = False
|
||||
if not inputdata:
|
||||
raise exc.InvalidArgumentException('missing input data')
|
||||
|
||||
if 'hw_addr' in inputdata:
|
||||
raise exc.InvalidArgumentException('hw_addr is a read only field')
|
||||
|
||||
if 'ipv4_address' not in inputdata:
|
||||
inputdata['ipv4_address'] = None
|
||||
|
||||
if 'ipv4_gateway' not in inputdata:
|
||||
inputdata['ipv4_gateway'] = None
|
||||
|
||||
if 'ipv4_configuration' in inputdata:
|
||||
if inputdata['ipv4_configuration'].lower() not in ['dhcp','static']:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Unrecognized ipv4_configuration')
|
||||
else:
|
||||
inputdata['ipv4_configuration'] = None
|
||||
if nodes is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'This only supports per-node input')
|
||||
for node in nodes:
|
||||
self.inputbynode[node] = inputdata
|
||||
|
||||
def netconfig(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class InputDomainName(ConfluentInputMessage):
|
||||
def __init__(self, path, nodes, inputdata):
|
||||
self.inputbynode = {}
|
||||
self.stripped = False
|
||||
if not inputdata or 'domain_name' not in inputdata:
|
||||
raise exc.InvalidArgumentException('missing input data')
|
||||
if len(inputdata['domain_name']) > 256:
|
||||
raise exc.InvalidArgumentException(
|
||||
'identifier must be less than or = 256 chars')
|
||||
|
||||
if nodes is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'This only supports per-node input')
|
||||
for node in nodes:
|
||||
self.inputbynode[node] = inputdata['domain_name']
|
||||
|
||||
def domain_name(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class InputNTPServer(ConfluentInputMessage):
|
||||
def __init__(self, path, nodes, inputdata):
|
||||
self.inputbynode = {}
|
||||
self.stripped = False
|
||||
if not inputdata or 'server' not in inputdata:
|
||||
raise exc.InvalidArgumentException('missing input data')
|
||||
if len(inputdata['server']) > 256:
|
||||
raise exc.InvalidArgumentException(
|
||||
'identifier must be less than or = 256 chars')
|
||||
|
||||
if nodes is None:
|
||||
raise exc.InvalidArgumentException(
|
||||
'This only supports per-node input')
|
||||
for node in nodes:
|
||||
self.inputbynode[node] = str(inputdata['server'])
|
||||
|
||||
def ntp_server(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class InputNTPEnabled(ConfluentInputMessage):
|
||||
valid_values = set([
|
||||
'True',
|
||||
'False'
|
||||
])
|
||||
|
||||
def ntp_enabled(self, node):
|
||||
return self.inputbynode[node]
|
||||
|
||||
|
||||
class BootDevice(ConfluentChoiceMessage):
|
||||
valid_values = set([
|
||||
'network',
|
||||
@@ -435,10 +704,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))
|
||||
@@ -448,7 +717,8 @@ class BootDevice(ConfluentChoiceMessage):
|
||||
self.kvpairs = {
|
||||
node: {
|
||||
'nextdevice': {'value': device},
|
||||
'bootmode': {'value': bootmode },
|
||||
'bootmode': {'value': bootmode},
|
||||
'persistent': {'value': persistent},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,6 +727,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:
|
||||
@@ -478,6 +749,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:
|
||||
@@ -491,6 +764,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]
|
||||
@@ -498,6 +773,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([
|
||||
@@ -514,10 +792,205 @@ class PowerState(ConfluentChoiceMessage):
|
||||
'off',
|
||||
'reset',
|
||||
'boot',
|
||||
'shutdown',
|
||||
'diag',
|
||||
])
|
||||
keyname = 'state'
|
||||
|
||||
|
||||
class BMCReset(ConfluentChoiceMessage):
|
||||
valid_values = set([
|
||||
'reset',
|
||||
])
|
||||
keyname = 'state'
|
||||
|
||||
|
||||
class NTPEnabled(ConfluentChoiceMessage):
|
||||
valid_values = set([
|
||||
'True',
|
||||
'False',
|
||||
])
|
||||
|
||||
def __init__(self, node, enabled):
|
||||
self.stripped = False
|
||||
self.kvpairs = {
|
||||
node: {
|
||||
'state': {'value': str(enabled)},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class EventCollection(ConfluentMessage):
|
||||
"""A collection of events
|
||||
|
||||
This conveys a representation of an iterable of events. The following
|
||||
fields are supported:
|
||||
id (some data giving the class of event without the specific data of the
|
||||
event. For example, 'overtemp (1000 degrees celsius)' would have
|
||||
the same 'id' as 'overtemp (200 degrees celsius)
|
||||
component (specific name of the component this event references if any)
|
||||
component_type (A description of the sort of device component is)
|
||||
event (A text description of the event that occurred)
|
||||
severity (The text 'ok', 'warning', 'critical', 'failed', or 'unknown')
|
||||
timestamp (ISO 8601 compliant timestamp if available)
|
||||
"""
|
||||
readonly = True
|
||||
|
||||
def __init__(self, events=(), name=None):
|
||||
eventdata = []
|
||||
self.notnode = name is None
|
||||
for event in events:
|
||||
entry = {
|
||||
'id': event.get('id', None),
|
||||
'component': event.get('component', None),
|
||||
'component_type': event.get('component_type', None),
|
||||
'event': event.get('event', None),
|
||||
'severity': event['severity'],
|
||||
'timestamp': event.get('timestamp', None),
|
||||
'record_id': event.get('record_id', None),
|
||||
}
|
||||
if event['severity'] not in valid_health_values:
|
||||
raise exc.NotImplementedException(
|
||||
'Invalid severity - ' + repr(event['severity']))
|
||||
eventdata.append(entry)
|
||||
if self.notnode:
|
||||
self.kvpairs = {'events': eventdata}
|
||||
else:
|
||||
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'
|
||||
self.stripped = False
|
||||
self.notnode = name is None
|
||||
kvpairs = {'username': {'value': username},
|
||||
'password': {'value': '', 'type': 'password'},
|
||||
'privilege_level': {'value': privilege_level},
|
||||
'enabled': {'value': ''}
|
||||
}
|
||||
if self.notnode:
|
||||
self.kvpairs = kvpairs
|
||||
else:
|
||||
self.kvpairs = {name: kvpairs}
|
||||
|
||||
|
||||
class UserCollection(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, users=(), name=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'list of users'
|
||||
userlist = []
|
||||
for user in users:
|
||||
entry = {
|
||||
'uid': user['uid'],
|
||||
'username': user['name'],
|
||||
'privilege_level': user['access']['privilege_level']
|
||||
}
|
||||
userlist.append(entry)
|
||||
if self.notnode:
|
||||
self.kvpairs = {'users': userlist}
|
||||
else:
|
||||
self.kvpairs = {name: {'users': userlist}}
|
||||
|
||||
|
||||
class AlertDestination(ConfluentMessage):
|
||||
def __init__(self, ip, acknowledge=False, acknowledge_timeout=None, retries=0, name=None):
|
||||
self.desc = 'foo'
|
||||
self.stripped = False
|
||||
self.notnode = name is None
|
||||
kvpairs = {'ip': {'value': ip},
|
||||
'acknowledge': {'value': acknowledge},
|
||||
'acknowledge_timeout': {'value': acknowledge_timeout},
|
||||
'retries': {'value': retries}}
|
||||
if self.notnode:
|
||||
self.kvpairs = kvpairs
|
||||
else:
|
||||
self.kvpairs = {name: kvpairs}
|
||||
|
||||
|
||||
class InputAlertDestination(ConfluentMessage):
|
||||
valid_alert_params = {
|
||||
'acknowledge': lambda x: False if type(x) in (unicode,str) and x.lower() == 'false' else bool(x),
|
||||
'acknowledge_timeout': lambda x: int(x) if x and x.isdigit() else None,
|
||||
'ip': lambda x: x,
|
||||
'retries': lambda x: int(x)
|
||||
}
|
||||
|
||||
def __init__(self, path, nodes, inputdata, multinode=False):
|
||||
self.alertcfg = {}
|
||||
if multinode: # keys are node names
|
||||
for node in inputdata:
|
||||
self.alertcfg[node] = inputdata[node]
|
||||
for key in inputdata[node]:
|
||||
if key not in self.valid_alert_params:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Unrecognized alert parameter ' + key)
|
||||
if isinstance(inputdata[node][key], dict):
|
||||
self.alertcfg[node][key] = \
|
||||
self.valid_alert_params[key](
|
||||
inputdata[node][key]['value'])
|
||||
else:
|
||||
self.alertcfg[node][key] = \
|
||||
self.valid_alert_params[key](inputdata[node][key])
|
||||
else:
|
||||
for key in inputdata:
|
||||
if key not in self.valid_alert_params:
|
||||
raise exc.InvalidArgumentException(
|
||||
'Unrecognized alert parameter ' + key)
|
||||
if isinstance(inputdata[key], dict):
|
||||
inputdata[key] = self.valid_alert_params[key](
|
||||
inputdata[key]['value'])
|
||||
else:
|
||||
inputdata[key] = self.valid_alert_params[key](
|
||||
inputdata[key])
|
||||
for node in nodes:
|
||||
self.alertcfg[node] = inputdata
|
||||
|
||||
def alert_params_by_node(self, node):
|
||||
return self.alertcfg[node]
|
||||
|
||||
|
||||
class SensorReadings(ConfluentMessage):
|
||||
@@ -527,15 +1000,19 @@ class SensorReadings(ConfluentMessage):
|
||||
readings = []
|
||||
self.notnode = name is None
|
||||
for sensor in sensors:
|
||||
sensordict = {'name': sensor['name']}
|
||||
if 'value' in sensor:
|
||||
sensordict['value'] = sensor['value']
|
||||
if 'units' in sensor:
|
||||
sensordict['units'] = sensor['units']
|
||||
if 'states' in sensor:
|
||||
sensordict['states'] = sensor['states']
|
||||
if 'health' in sensor:
|
||||
sensordict['health'] = sensor['health']
|
||||
sensordict = {'name': sensor.name}
|
||||
if hasattr(sensor, 'value'):
|
||||
sensordict['value'] = sensor.value
|
||||
if hasattr(sensor, 'units'):
|
||||
sensordict['units'] = sensor.units
|
||||
if hasattr(sensor, 'states'):
|
||||
sensordict['states'] = sensor.states
|
||||
if hasattr(sensor, 'state_ids'):
|
||||
sensordict['state_ids'] = sensor.state_ids
|
||||
if hasattr(sensor, 'health'):
|
||||
sensordict['health'] = sensor.health
|
||||
if hasattr(sensor, 'type'):
|
||||
sensordict['type'] = sensor.type
|
||||
readings.append(sensordict)
|
||||
if self.notnode:
|
||||
self.kvpairs = {'sensors': readings}
|
||||
@@ -543,14 +1020,65 @@ class SensorReadings(ConfluentMessage):
|
||||
self.kvpairs = {name: {'sensors': readings}}
|
||||
|
||||
|
||||
class Firmware(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, data, name):
|
||||
self.notnode = name is None
|
||||
self.desc = 'Firmware information'
|
||||
if self.notnode:
|
||||
self.kvpairs = {'firmware': data}
|
||||
else:
|
||||
self.kvpairs = {name: {'firmware': data}}
|
||||
|
||||
|
||||
class KeyValueData(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, kvdata, name=None):
|
||||
self.notnode = name is None
|
||||
if self.notnode:
|
||||
self.kvpairs = kvdata
|
||||
else:
|
||||
self.kvpairs = {name: kvdata}
|
||||
|
||||
|
||||
class LEDStatus(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, data, name):
|
||||
self.notnode = name is None
|
||||
self.desc = 'led status'
|
||||
|
||||
if self.notnode:
|
||||
self.kvpairs = {'leds':data}
|
||||
else:
|
||||
self.kvpairs = {name: {'leds':data}}
|
||||
|
||||
|
||||
class NetworkConfiguration(ConfluentMessage):
|
||||
desc = 'Network configuration'
|
||||
|
||||
def __init__(self, name=None, ipv4addr=None, ipv4gateway=None,
|
||||
ipv4cfgmethod=None, hwaddr=None):
|
||||
self.notnode = name is None
|
||||
self.stripped = False
|
||||
|
||||
kvpairs = {
|
||||
'ipv4_address': {'value': ipv4addr},
|
||||
'ipv4_gateway': {'value': ipv4gateway},
|
||||
'ipv4_configuration': {'value': ipv4cfgmethod},
|
||||
'hw_addr': {'value': hwaddr},
|
||||
}
|
||||
if self.notnode:
|
||||
self.kvpairs = kvpairs
|
||||
else:
|
||||
self.kvpairs = {name: kvpairs}
|
||||
|
||||
|
||||
class HealthSummary(ConfluentMessage):
|
||||
readonly = True
|
||||
valid_values = set([
|
||||
'ok',
|
||||
'warning',
|
||||
'critical',
|
||||
'failed',
|
||||
])
|
||||
valid_values = valid_health_values
|
||||
|
||||
def __init__(self, health, name=None):
|
||||
self.stripped = False
|
||||
@@ -588,7 +1116,76 @@ class ListAttributes(ConfluentMessage):
|
||||
if self.notnode:
|
||||
self.kvpairs = kv
|
||||
else:
|
||||
self.kvpairs = {node: kv}
|
||||
self.kvpairs = {name: kv}
|
||||
|
||||
|
||||
class MCI(ConfluentMessage):
|
||||
def __init__(self, name=None, mci=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'BMC identifier'
|
||||
|
||||
kv = {'identifier': {'value': mci}}
|
||||
if self.notnode:
|
||||
self.kvpairs = kv
|
||||
else:
|
||||
self.kvpairs = {name: kv}
|
||||
|
||||
|
||||
class DomainName(ConfluentMessage):
|
||||
def __init__(self, name=None, dn=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'BMC domain name'
|
||||
|
||||
kv = {'domain_name': {'value': dn}}
|
||||
if self.notnode:
|
||||
self.kvpairs = kv
|
||||
else:
|
||||
self.kvpairs = {name: kv}
|
||||
|
||||
|
||||
class NTPServers(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, name=None, servers=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'NTP Server'
|
||||
|
||||
kv = []
|
||||
for idx in range(0, len(servers)):
|
||||
kv.append({str(idx+1): servers[idx]})
|
||||
if self.notnode:
|
||||
self.kvpairs = {'ntp_servers': kv}
|
||||
else:
|
||||
self.kvpairs = {name: {'ntp_servers': kv}}
|
||||
|
||||
|
||||
class NTPServer(ConfluentMessage):
|
||||
def __init__(self, name=None, server=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'NTP Server'
|
||||
|
||||
kv = {
|
||||
'server': {'value': server},
|
||||
}
|
||||
if self.notnode:
|
||||
self.kvpairs = kv
|
||||
else:
|
||||
self.kvpairs = {name: kv}
|
||||
|
||||
|
||||
class License(ConfluentMessage):
|
||||
readonly = True
|
||||
|
||||
def __init__(self, name=None, kvm=None):
|
||||
self.notnode = name is None
|
||||
self.desc = 'License'
|
||||
|
||||
kv = []
|
||||
kv.append({'kvm_availability': str(kvm)})
|
||||
if self.notnode:
|
||||
self.kvpairs = {'License': kv}
|
||||
else:
|
||||
self.kvpairs = {name: {'License': kv}}
|
||||
|
||||
|
||||
class CryptedAttributes(Attributes):
|
||||
@@ -611,5 +1208,5 @@ class CryptedAttributes(Attributes):
|
||||
self.kvpairs = nkv
|
||||
else:
|
||||
self.kvpairs = {
|
||||
node: nkv
|
||||
name: nkv
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
# 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])
|
||||
@@ -20,12 +20,13 @@
|
||||
# the middle of strings and use of @ for anything is not in their syntax
|
||||
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import pyparsing as pp
|
||||
import re
|
||||
|
||||
# construct custom grammar with pyparsing
|
||||
_nodeword = pp.Word(pp.alphanums + '~^$/=-:.*+!')
|
||||
_nodeword = pp.Word(pp.alphanums + '~^$/=-_:.*+!')
|
||||
_nodebracket = pp.QuotedString(quoteChar='[', endQuoteChar=']',
|
||||
unquoteResults=False)
|
||||
_nodeatom = pp.Group(pp.OneOrMore(_nodeword | _nodebracket))
|
||||
@@ -160,7 +161,7 @@ class NodeRange(object):
|
||||
return set([entname])
|
||||
if self.cfm.is_nodegroup(entname):
|
||||
grpcfg = self.cfm.get_nodegroup_attributes(entname)
|
||||
nodes = grpcfg['nodes']
|
||||
nodes = copy.copy(grpcfg['nodes'])
|
||||
if 'noderange' in grpcfg and grpcfg['noderange']:
|
||||
nodes |= NodeRange(
|
||||
grpcfg['noderange']['value'], self.cfm).nodes
|
||||
@@ -185,7 +186,7 @@ class NodeRange(object):
|
||||
return set([element])
|
||||
if self.cfm.is_nodegroup(element):
|
||||
grpcfg = self.cfm.get_nodegroup_attributes(element)
|
||||
nodes = grpcfg['nodes']
|
||||
nodes = copy.copy(grpcfg['nodes'])
|
||||
if 'noderange' in grpcfg and grpcfg['noderange']:
|
||||
nodes |= NodeRange(
|
||||
grpcfg['noderange']['value'], self.cfm).nodes
|
||||
|
||||
@@ -152,6 +152,20 @@ 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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 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.
|
||||
@@ -13,22 +13,32 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import atexit
|
||||
import confluent.exceptions as exc
|
||||
import confluent.interface.console as conapi
|
||||
import confluent.messages as msg
|
||||
import confluent.util as util
|
||||
import eventlet
|
||||
import eventlet.event
|
||||
import eventlet.green.threading as threading
|
||||
import eventlet.greenpool as greenpool
|
||||
import eventlet.queue as queue
|
||||
import eventlet.support.greendns
|
||||
import pyghmi.constants as pygconstants
|
||||
import pyghmi.exceptions as pygexc
|
||||
import pyghmi.ipmi.console as console
|
||||
import pyghmi.ipmi.command as ipmicommand
|
||||
console = eventlet.import_patched('pyghmi.ipmi.console')
|
||||
ipmicommand = eventlet.import_patched('pyghmi.ipmi.command')
|
||||
import socket
|
||||
|
||||
console.session.select = eventlet.green.select
|
||||
console.session.threading = eventlet.green.threading
|
||||
console.session.socket.getaddrinfo = eventlet.support.greendns.getaddrinfo
|
||||
|
||||
|
||||
def exithandler():
|
||||
console.session.iothread.join()
|
||||
|
||||
atexit.register(exithandler)
|
||||
|
||||
_ipmiworkers = greenpool.GreenPool()
|
||||
|
||||
@@ -42,18 +52,67 @@ sensor_categories = {
|
||||
}
|
||||
|
||||
|
||||
def hex2bin(hexstring):
|
||||
hexvals = hexstring.split(':')
|
||||
if len(hexvals) < 2:
|
||||
hexvals = hexstring.split(' ')
|
||||
if len(hexvals) < 2:
|
||||
hexvals = [hexstring[i:i+2] for i in xrange(0, len(hexstring), 2)]
|
||||
bytedata = [int(i, 16) for i in hexvals]
|
||||
return bytearray(bytedata)
|
||||
|
||||
|
||||
def simplify_name(name):
|
||||
return name.lower().replace(' ', '_')
|
||||
return name.lower().replace(' ', '_').replace('/', '-')
|
||||
|
||||
|
||||
def sanitize_invdata(indata):
|
||||
"""Sanitize pyghmi data
|
||||
|
||||
pyghmi will return bytearrays when it has no idea what to do. In our
|
||||
case, we will change those to hex strings. Additionally, ignore 'extra'
|
||||
fields if the oem_parser is set
|
||||
"""
|
||||
if 'oem_parser' in indata and indata['oem_parser'] is not None:
|
||||
if 'board_extra' in indata:
|
||||
del indata['board_extra']
|
||||
if 'chassis_extra' in indata:
|
||||
del indata['chassis_extra']
|
||||
if 'product_extra' in indata:
|
||||
del indata['product_extra']
|
||||
for k in indata:
|
||||
if isinstance(indata[k], bytearray):
|
||||
indata[k] = '0x' + ''.join(format(x, '02x') for x in indata[k])
|
||||
elif isinstance(indata[k], dict):
|
||||
sanitize_invdata(indata[k])
|
||||
elif isinstance(indata[k], list):
|
||||
for idx, value in enumerate(indata[k]):
|
||||
if isinstance(value, bytearray):
|
||||
indata[k][idx] = '0x' + ''.join(
|
||||
format(x, '02x') for x in indata[k][idx])
|
||||
|
||||
|
||||
class IpmiCommandWrapper(ipmicommand.Command):
|
||||
def __init__(self, node, cfm, **kwargs):
|
||||
self.cfm = cfm
|
||||
self.node = node
|
||||
self._inhealth = False
|
||||
self._lasthealth = None
|
||||
self._attribwatcher = cfm.watch_attributes(
|
||||
(node,), ('secret.hardwaremanagementuser',
|
||||
'secret.hardwaremanagementpassword', 'secret.ipmikg',
|
||||
'hardwaremanagement.manager'), self._attribschanged)
|
||||
super(self.__class__, self).__init__(**kwargs)
|
||||
|
||||
def setup_confluent_keyhandler(self):
|
||||
self.register_key_handler(util.TLSCertVerifier(
|
||||
self.cfm, self.node, 'pubkeys.tls_hardwaremanager').verify_cert)
|
||||
|
||||
def close_confluent(self):
|
||||
if self._attribwatcher:
|
||||
self.cfm.remove_watcher(self._attribwatcher)
|
||||
self._attribwatcher = None
|
||||
|
||||
def _attribschanged(self, nodeattribs, configmanager, **kwargs):
|
||||
try:
|
||||
self.ipmi_session._mark_broken()
|
||||
@@ -62,6 +121,20 @@ class IpmiCommandWrapper(ipmicommand.Command):
|
||||
# then do nothing
|
||||
pass
|
||||
|
||||
def get_health(self):
|
||||
if self._inhealth:
|
||||
while self._inhealth:
|
||||
eventlet.sleep(0.1)
|
||||
return self._lasthealth
|
||||
self._inhealth = True
|
||||
try:
|
||||
self._lasthealth = super(IpmiCommandWrapper, self).get_health()
|
||||
except Exception:
|
||||
self._inhealth = False
|
||||
raise
|
||||
self._inhealth = False
|
||||
return self._lasthealth
|
||||
|
||||
|
||||
def _ipmi_evtloop():
|
||||
while True:
|
||||
@@ -146,6 +219,7 @@ class IpmiConsole(conapi.Console):
|
||||
self.broken = True
|
||||
self.error = data['error']
|
||||
if self.connected:
|
||||
self.connected = False
|
||||
self.datacallback(conapi.ConsoleEvent.Disconnect)
|
||||
else:
|
||||
self.datacallback(data)
|
||||
@@ -223,22 +297,22 @@ def perform_request(operator, node, element,
|
||||
results.put(msg.ConfluentTargetTimeout(node))
|
||||
else:
|
||||
results.put(msg.ConfluentNodeError(node, excmsg))
|
||||
raise
|
||||
except exc.TargetEndpointUnreachable as tu:
|
||||
results.put(msg.ConfluentTargetTimeout(node, str(tu)))
|
||||
except Exception as e:
|
||||
results.put(msg.ConfluentNodeError(
|
||||
node, 'IPMI PluginException (see stderr log): ' + str(e)))
|
||||
raise
|
||||
finally:
|
||||
results.put('Done')
|
||||
|
||||
persistent_ipmicmds = {}
|
||||
|
||||
|
||||
def _dict_sensor(pygreading):
|
||||
retdict = {'name': pygreading.name, 'value': pygreading.value,
|
||||
'states': pygreading.states, 'units': pygreading.units,
|
||||
'health': _str_health(pygreading.health)}
|
||||
return retdict
|
||||
|
||||
|
||||
class IpmiHandler(object):
|
||||
def __init__(self, operation, node, element, cfd, inputdata, cfg, output):
|
||||
self.sensormap = {}
|
||||
self.invmap = {}
|
||||
self.output = output
|
||||
self.sensorcategory = None
|
||||
self.broken = False
|
||||
@@ -257,6 +331,10 @@ class IpmiHandler(object):
|
||||
if ((node, tenant) not in persistent_ipmicmds or
|
||||
not persistent_ipmicmds[(node, tenant)].ipmi_session.logged):
|
||||
self._logevt = threading.Event()
|
||||
try:
|
||||
persistent_ipmicmds[(node, tenant)].close_confluent()
|
||||
except KeyError: # was no previous session
|
||||
pass
|
||||
try:
|
||||
persistent_ipmicmds[(node, tenant)] = IpmiCommandWrapper(
|
||||
node, cfg, bmc=connparams['bmc'],
|
||||
@@ -266,6 +344,7 @@ 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 = {
|
||||
@@ -277,7 +356,9 @@ class IpmiHandler(object):
|
||||
self.broken = True
|
||||
self.error = response['error']
|
||||
else:
|
||||
self.ipmicmd = ipmicmd
|
||||
self.loggedin = True
|
||||
self.ipmicmd.setup_confluent_keyhandler()
|
||||
self._logevt.set()
|
||||
|
||||
def handle_request(self):
|
||||
@@ -285,12 +366,17 @@ class IpmiHandler(object):
|
||||
self._logevt.wait()
|
||||
self._logevt = None
|
||||
if self.broken:
|
||||
if self.error == 'timeout':
|
||||
self.output.put(msg.ConfluentTargetTimeout(self.node))
|
||||
if (self.error == 'timeout' or
|
||||
'Insufficient resources' in self.error):
|
||||
self.error = self.error.replace(' reported in RAKP4', '')
|
||||
self.output.put(msg.ConfluentTargetTimeout(
|
||||
self.node, self.error))
|
||||
return
|
||||
elif ('Unauthorized' in self.error or
|
||||
'Incorrect password' in self.error):
|
||||
self.output.put(
|
||||
msg.ConfluentTargetInvalidCredentials(self.node))
|
||||
return
|
||||
else:
|
||||
raise Exception(self.error)
|
||||
if self.element == ['power', 'state']:
|
||||
@@ -303,6 +389,190 @@ class IpmiHandler(object):
|
||||
self.identify()
|
||||
elif self.element[0] == 'sensors':
|
||||
self.handle_sensors()
|
||||
elif self.element[0] == 'configuration':
|
||||
self.handle_configuration()
|
||||
elif self.element[0] == 'inventory':
|
||||
self.handle_inventory()
|
||||
elif self.element == ['events', 'hardware', 'log']:
|
||||
self.do_eventlog()
|
||||
elif self.element == ['events', 'hardware', 'decode']:
|
||||
self.decode_alert()
|
||||
elif self.element == ['console', 'license']:
|
||||
self.handle_license()
|
||||
else:
|
||||
raise Exception('Not Implemented')
|
||||
|
||||
def handle_configuration(self):
|
||||
if self.element[1:3] == ['management_controller', 'alerts']:
|
||||
return self.handle_alerts()
|
||||
elif self.element[1:3] == ['management_controller', 'users']:
|
||||
return self.handle_users()
|
||||
elif self.element[1:3] == ['management_controller', 'net_interfaces']:
|
||||
return self.handle_nets()
|
||||
elif self.element[1:3] == ['management_controller', 'reset']:
|
||||
return self.handle_reset()
|
||||
elif self.element[1:3] == ['management_controller', 'identifier']:
|
||||
return self.handle_identifier()
|
||||
elif self.element[1:3] == ['management_controller', 'domain_name']:
|
||||
return self.handle_domain_name()
|
||||
elif self.element[1:3] == ['management_controller', 'ntp']:
|
||||
return self.handle_ntp()
|
||||
raise Exception('Not implemented')
|
||||
|
||||
def decode_alert(self):
|
||||
inputdata = self.inputdata.get_alert(self.node)
|
||||
specifictrap = int(inputdata['.1.3.6.1.6.3.1.1.4.1.0'].rpartition(
|
||||
'.')[-1])
|
||||
for tmpvarbind in inputdata:
|
||||
if tmpvarbind.endswith('3183.1.1'):
|
||||
varbinddata = inputdata[tmpvarbind]
|
||||
varbinddata = hex2bin(varbinddata)
|
||||
event = self.ipmicmd.decode_pet(specifictrap, varbinddata)
|
||||
self.pyghmi_event_to_confluent(event)
|
||||
self.output.put(msg.EventCollection((event,), name=self.node))
|
||||
|
||||
def handle_alerts(self):
|
||||
if self.element[3] == 'destinations':
|
||||
if len(self.element) == 4:
|
||||
# A list of destinations
|
||||
maxdest = self.ipmicmd.get_alert_destination_count()
|
||||
for alertidx in xrange(0, maxdest + 1):
|
||||
self.output.put(msg.ChildCollection(alertidx))
|
||||
return
|
||||
elif len(self.element) == 5:
|
||||
alertidx = int(self.element[-1])
|
||||
if self.op == 'read':
|
||||
destdata = self.ipmicmd.get_alert_destination(alertidx)
|
||||
self.output.put(msg.AlertDestination(
|
||||
ip=destdata['address'],
|
||||
acknowledge=destdata['acknowledge_required'],
|
||||
acknowledge_timeout=destdata.get('acknowledge_timeout', None),
|
||||
retries=destdata['retries'],
|
||||
name=self.node))
|
||||
return
|
||||
elif self.op == 'update':
|
||||
alertparms = self.inputdata.alert_params_by_node(
|
||||
self.node)
|
||||
alertargs = {}
|
||||
if 'acknowledge' in alertparms:
|
||||
alertargs['acknowledge_required'] = alertparms['acknowledge']
|
||||
if 'acknowledge_timeout' in alertparms:
|
||||
alertargs['acknowledge_timeout'] = alertparms['acknowledge_timeout']
|
||||
if 'ip' in alertparms:
|
||||
alertargs['ip'] = alertparms['ip']
|
||||
if 'retries' in alertparms:
|
||||
alertargs['retries'] = alertparms['retries']
|
||||
self.ipmicmd.set_alert_destination(destination=alertidx,
|
||||
**alertargs)
|
||||
return
|
||||
elif self.op == 'delete':
|
||||
self.ipmicmd.clear_alert_destination(alertidx)
|
||||
return
|
||||
raise Exception('Not implemented')
|
||||
|
||||
def handle_nets(self):
|
||||
if len(self.element) == 3:
|
||||
if self.op != 'read':
|
||||
self.output.put(
|
||||
msg.ConfluentNodeError(self.node, 'Unsupported operation'))
|
||||
return
|
||||
self.output.put(msg.ChildCollection('management'))
|
||||
elif len(self.element) == 4 and self.element[-1] == 'management':
|
||||
if self.op == 'read':
|
||||
lancfg = self.ipmicmd.get_net_configuration()
|
||||
self.output.put(msg.NetworkConfiguration(
|
||||
self.node, ipv4addr=lancfg['ipv4_address'],
|
||||
ipv4gateway=lancfg['ipv4_gateway'],
|
||||
ipv4cfgmethod=lancfg['ipv4_configuration'],
|
||||
hwaddr=lancfg['mac_address']
|
||||
))
|
||||
elif self.op == 'update':
|
||||
config = self.inputdata.netconfig(self.node)
|
||||
self.ipmicmd.set_net_configuration(
|
||||
ipv4_address=config['ipv4_address'],
|
||||
ipv4_configuration=config['ipv4_configuration'],
|
||||
ipv4_gateway=config['ipv4_gateway'])
|
||||
|
||||
def handle_users(self):
|
||||
# Create user
|
||||
if len(self.element) == 3:
|
||||
if self.op == 'update':
|
||||
user = self.inputdata.credentials[self.node]
|
||||
self.ipmicmd.create_user(uid=user['uid'], name=user['username'],
|
||||
password=user['password'],
|
||||
callback=True,link_auth=True, ipmi_msg=True,
|
||||
privilege_level=user['privilege_level'])
|
||||
# A list of users
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
for user in self.ipmicmd.get_users():
|
||||
self.output.put(msg.ChildCollection(user, candelete=True))
|
||||
return
|
||||
# List all users
|
||||
elif len(self.element) == 4 and self.element[-1] == 'all':
|
||||
users = []
|
||||
for user in self.ipmicmd.get_users():
|
||||
users.append(self.ipmicmd.get_user(uid=user))
|
||||
self.output.put(msg.UserCollection(users=users, name=self.node))
|
||||
return
|
||||
# Update user
|
||||
elif len(self.element) == 4:
|
||||
user = int(self.element[-1])
|
||||
if self.op == 'read':
|
||||
data = self.ipmicmd.get_user(uid=user)
|
||||
self.output.put(msg.User(
|
||||
uid=data['uid'],
|
||||
username=data['name'],
|
||||
privilege_level=data['access']['privilege_level'],
|
||||
name=self.node))
|
||||
return
|
||||
elif self.op == 'update':
|
||||
user = self.inputdata.credentials[self.node]
|
||||
|
||||
if 'username' in user:
|
||||
self.ipmicmd.set_user_name(uid=user['uid'],
|
||||
name=user['username'])
|
||||
if 'privilege_level' in user:
|
||||
self.ipmicmd.set_user_access(uid=user['uid'],
|
||||
privilege_level=user['privilege_level'])
|
||||
if 'password' in user:
|
||||
self.ipmicmd.set_user_password(uid=user['uid'],
|
||||
password=user['password'])
|
||||
self.ipmicmd.set_user_password(uid=user['uid'],
|
||||
mode='enable', password=user['password'])
|
||||
if 'enabled' in user:
|
||||
if user['enabled'] == 'yes':
|
||||
mode = 'enable'
|
||||
else:
|
||||
mode = 'disable'
|
||||
self.ipmicmd.disable_user(user['uid'], mode)
|
||||
return
|
||||
elif self.op == 'delete':
|
||||
self.ipmicmd.user_delete(uid=user)
|
||||
return
|
||||
|
||||
def do_eventlog(self):
|
||||
eventout = []
|
||||
clear = False
|
||||
if self.op == 'delete':
|
||||
clear = True
|
||||
for event in self.ipmicmd.get_event_log(clear):
|
||||
self.pyghmi_event_to_confluent(event)
|
||||
eventout.append(event)
|
||||
self.output.put(msg.EventCollection(eventout, name=self.node))
|
||||
|
||||
def pyghmi_event_to_confluent(self, event):
|
||||
event['severity'] = _str_health(event.get('severity', 'unknown'))
|
||||
if 'event_data' in event:
|
||||
event['event'] = '{0} - {1}'.format(
|
||||
event['event'], event['event_data'])
|
||||
if 'event_id' in event:
|
||||
event['id'] = '{0}.{1}'.format(event['event_id'],
|
||||
event['component_type_id'])
|
||||
|
||||
def make_inventory_map(self):
|
||||
invnames = self.ipmicmd.get_inventory_descriptions()
|
||||
for name in invnames:
|
||||
self.invmap[simplify_name(name)] = name
|
||||
|
||||
def make_sensor_map(self, sensors=None):
|
||||
if sensors is None:
|
||||
@@ -324,7 +594,9 @@ class IpmiHandler(object):
|
||||
if ie.ipmicode == 203:
|
||||
continue
|
||||
raise
|
||||
readings.append(_dict_sensor(reading))
|
||||
if hasattr(reading, 'health'):
|
||||
reading.health = _str_health(reading.health)
|
||||
readings.append(reading)
|
||||
self.output.put(msg.SensorReadings(readings, name=self.node))
|
||||
else:
|
||||
self.make_sensor_map()
|
||||
@@ -335,21 +607,103 @@ class IpmiHandler(object):
|
||||
return
|
||||
reading = self.ipmicmd.get_sensor_reading(
|
||||
self.sensormap[sensorname])
|
||||
if hasattr(reading, 'health'):
|
||||
reading.health = _str_health(reading.health)
|
||||
self.output.put(
|
||||
msg.SensorReadings([_dict_sensor(reading)],
|
||||
msg.SensorReadings([reading],
|
||||
name=self.node))
|
||||
except pygexc.IpmiException:
|
||||
self.output.put(msg.ConfluentTargetTimeout(self.node))
|
||||
|
||||
def list_inventory(self):
|
||||
try:
|
||||
components = self.ipmicmd.get_inventory_descriptions()
|
||||
except pygexc.IpmiException:
|
||||
self.output.put(msg.ConfluentTargetTimeout(self.node))
|
||||
return
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
for component in components:
|
||||
self.output.put(msg.ChildCollection(simplify_name(component)))
|
||||
|
||||
def list_firmware(self):
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
for id, data in self.ipmicmd.get_firmware():
|
||||
self.output.put(msg.ChildCollection(simplify_name(id)))
|
||||
|
||||
def read_firmware(self, component):
|
||||
items = []
|
||||
for id, data in self.ipmicmd.get_firmware():
|
||||
if component == 'all' or component == simplify_name(id):
|
||||
items.append({id: data})
|
||||
self.output.put(msg.Firmware(items, self.node))
|
||||
|
||||
def handle_inventory(self):
|
||||
if self.element[1] == 'firmware':
|
||||
if len(self.element) == 3:
|
||||
return self.list_firmware()
|
||||
elif len(self.element) == 4:
|
||||
return self.read_firmware(self.element[-1])
|
||||
elif self.element[1] == 'hardware':
|
||||
if len(self.element) == 3: # list things in inventory
|
||||
return self.list_inventory()
|
||||
elif len(self.element) == 4: # actually read inventory data
|
||||
return self.read_inventory(self.element[-1])
|
||||
raise Exception('Unsupported scenario...')
|
||||
|
||||
def list_leds(self):
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
for category, info in self.ipmicmd.get_leds():
|
||||
self.output.put(msg.ChildCollection(simplify_name(category)))
|
||||
|
||||
def read_leds(self, component):
|
||||
led_categories = []
|
||||
for category, info in self.ipmicmd.get_leds():
|
||||
if component == 'all' or component == simplify_name(category):
|
||||
led_categories.append({category: info})
|
||||
self.output.put(msg.LEDStatus(led_categories, self.node))
|
||||
|
||||
def read_inventory(self, component):
|
||||
invitems = []
|
||||
if component == 'all':
|
||||
for invdata in self.ipmicmd.get_inventory():
|
||||
if invdata[1] is None:
|
||||
newinf = {'present': False, 'information': None}
|
||||
else:
|
||||
sanitize_invdata(invdata[1])
|
||||
newinf = {'present': True, 'information': invdata[1]}
|
||||
newinf['name'] = invdata[0]
|
||||
invitems.append(newinf)
|
||||
else:
|
||||
self.make_inventory_map()
|
||||
compname = self.invmap.get(component, None)
|
||||
if compname is None:
|
||||
self.output.put(msg.ConfluentTargetNotFound())
|
||||
return
|
||||
invdata = self.ipmicmd.get_inventory_of_component(compname)
|
||||
if invdata is None:
|
||||
newinf = {'present': False, 'information': None}
|
||||
else:
|
||||
sanitize_invdata(invdata)
|
||||
newinf = {'present': True, 'information': invdata}
|
||||
newinf['name'] = compname
|
||||
invitems.append(newinf)
|
||||
newinvdata = {'inventory': invitems}
|
||||
self.output.put(msg.KeyValueData(newinvdata, self.node))
|
||||
|
||||
def handle_sensors(self):
|
||||
if self.element[-1] == '':
|
||||
self.element = self.element[:-1]
|
||||
if len(self.element) < 3:
|
||||
return
|
||||
self.sensorcategory = self.element[2]
|
||||
if len(self.element) == 3: # list sensors per category
|
||||
# list sensors per category
|
||||
if len(self.element) == 3 and self.element[-2] == 'hardware':
|
||||
if self.sensorcategory == 'leds':
|
||||
return self.list_leds()
|
||||
return self.list_sensors()
|
||||
elif len(self.element) == 4: # resource requested
|
||||
if self.sensorcategory == 'leds':
|
||||
return self.read_leds(self.element[-1])
|
||||
return self.read_sensors(self.element[-1])
|
||||
|
||||
def match_sensor(self, sensor):
|
||||
@@ -382,7 +736,9 @@ class IpmiHandler(object):
|
||||
if 'badreadings' in response:
|
||||
badsensors = []
|
||||
for reading in response['badreadings']:
|
||||
badsensors.append(_dict_sensor(reading))
|
||||
if hasattr(reading, 'health'):
|
||||
reading.health = _str_health(reading.health)
|
||||
badsensors.append(reading)
|
||||
self.output.put(msg.SensorReadings(badsensors, name=self.node))
|
||||
else:
|
||||
raise exc.InvalidArgumentException('health is read-only')
|
||||
@@ -398,15 +754,22 @@ class IpmiHandler(object):
|
||||
bootmode = 'uefi'
|
||||
else:
|
||||
bootmode = 'bios'
|
||||
persistent = False
|
||||
if 'persistent' in bootdev:
|
||||
persistent = bootdev['persistent']
|
||||
self.output.put(msg.BootDevice(node=self.node,
|
||||
device=bootdev['bootdev'],
|
||||
bootmode=bootmode))
|
||||
bootmode=bootmode,
|
||||
persistent=persistent))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
bootdev = self.inputdata.bootdevice(self.node)
|
||||
douefi = False
|
||||
if self.inputdata.bootmode(self.node) == 'uefi':
|
||||
douefi = True
|
||||
bootdev = self.ipmicmd.set_bootdev(bootdev, uefiboot=douefi)
|
||||
persistent = self.inputdata.persistent(self.node)
|
||||
bootdev = self.ipmicmd.set_bootdev(bootdev, uefiboot=douefi,
|
||||
persist=persistent)
|
||||
if bootdev['bootdev'] in self.bootdevices:
|
||||
bootdev['bootdev'] = self.bootdevices[bootdev['bootdev']]
|
||||
self.output.put(msg.BootDevice(node=self.node,
|
||||
@@ -418,24 +781,108 @@ class IpmiHandler(object):
|
||||
self.ipmicmd.set_identify(on=identifystate)
|
||||
self.output.put(msg.IdentifyState(
|
||||
node=self.node, state=self.inputdata.inputbynode[self.node]))
|
||||
return
|
||||
elif 'read' == self.op:
|
||||
# ipmi has identify as read-only for now
|
||||
self.output.put(msg.IdentifyState(node=self.node, state=''))
|
||||
return
|
||||
|
||||
def power(self):
|
||||
if 'read' == self.op:
|
||||
power = self.ipmicmd.get_power()
|
||||
self.output.put(msg.PowerState(node=self.node,
|
||||
state=power['powerstate']))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
powerstate = self.inputdata.powerstate(self.node)
|
||||
self.ipmicmd.set_power(powerstate, wait=30)
|
||||
power = self.ipmicmd.get_power()
|
||||
self.output.put(msg.PowerState(node=self.node,
|
||||
state=power['powerstate']))
|
||||
return
|
||||
|
||||
def handle_reset(self):
|
||||
if 'read' == self.op:
|
||||
self.output.put(msg.BMCReset(node=self.node,
|
||||
state='reset'))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
self.ipmicmd.reset_bmc()
|
||||
return
|
||||
|
||||
def handle_identifier(self):
|
||||
if 'read' == self.op:
|
||||
mci = self.ipmicmd.get_mci()
|
||||
self.output.put(msg.MCI(self.node, mci))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
mci = self.inputdata.mci(self.node)
|
||||
self.ipmicmd.set_mci(mci)
|
||||
return
|
||||
|
||||
def handle_domain_name(self):
|
||||
if 'read' == self.op:
|
||||
dn = self.ipmicmd.get_domain_name()
|
||||
self.output.put(msg.DomainName(self.node, dn))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
dn = self.inputdata.domain_name(self.node)
|
||||
self.ipmicmd.set_domain_name(dn)
|
||||
return
|
||||
|
||||
def handle_ntp(self):
|
||||
if self.element[3] == 'enabled':
|
||||
if 'read' == self.op:
|
||||
enabled = self.ipmicmd.get_ntp_enabled()
|
||||
self.output.put(msg.NTPEnabled(self.node, enabled))
|
||||
return
|
||||
elif 'update' == self.op:
|
||||
enabled = self.inputdata.ntp_enabled(self.node)
|
||||
self.ipmicmd.set_ntp_enabled(enabled == 'True')
|
||||
return
|
||||
elif self.element[3] == 'servers':
|
||||
if len(self.element) == 4:
|
||||
self.output.put(msg.ChildCollection('all'))
|
||||
size = len(self.ipmicmd.get_ntp_servers())
|
||||
for idx in range(1, size + 1):
|
||||
self.output.put(msg.ChildCollection(idx))
|
||||
else:
|
||||
if 'read' == self.op:
|
||||
if self.element[-1] == 'all':
|
||||
servers = self.ipmicmd.get_ntp_servers()
|
||||
self.output.put(msg.NTPServers(self.node, servers))
|
||||
return
|
||||
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'))
|
||||
return
|
||||
elif self.op in ('update', 'create'):
|
||||
if self.element[-1] == 'all':
|
||||
servers = self.inputdata.ntp_servers(self.node)
|
||||
for idx in servers:
|
||||
self.ipmicmd.set_ntp_server(servers[idx],
|
||||
int(idx[-1])-1)
|
||||
return
|
||||
else:
|
||||
idx = int(self.element[-1]) - 1
|
||||
server = self.inputdata.ntp_server(self.node)
|
||||
self.ipmicmd.set_ntp_server(server, idx)
|
||||
return
|
||||
|
||||
def handle_license(self):
|
||||
available = self.ipmicmd.get_remote_kvm_available()
|
||||
self.output.put(msg.License(self.node, available))
|
||||
return
|
||||
|
||||
def _str_health(health):
|
||||
if isinstance(health, str):
|
||||
return health
|
||||
if pygconstants.Health.Failed & health:
|
||||
health = 'failed'
|
||||
elif pygconstants.Health.Critical & health:
|
||||
@@ -471,4 +918,9 @@ def update(nodes, element, configmanager, inputdata):
|
||||
|
||||
def retrieve(nodes, element, configmanager, inputdata):
|
||||
initthread()
|
||||
return perform_requests('read', nodes, element, configmanager, inputdata)
|
||||
return perform_requests('read', nodes, element, configmanager, inputdata)
|
||||
|
||||
def delete(nodes, element, configmanager, inputdata):
|
||||
initthread()
|
||||
return perform_requests(
|
||||
'delete', nodes, element, configmanager, inputdata)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
|
||||
|
||||
# This plugin provides an ssh implementation comforming to the 'console'
|
||||
# specification. consoleserver or shellserver would be equally likely
|
||||
# to use this.
|
||||
|
||||
import confluent.exceptions as cexc
|
||||
import confluent.interface.console as conapi
|
||||
import confluent.log as log
|
||||
import eventlet
|
||||
import hashlib
|
||||
paramiko = eventlet.import_patched('paramiko')
|
||||
|
||||
|
||||
class HostKeyHandler(paramiko.client.MissingHostKeyPolicy):
|
||||
|
||||
def __init__(self, configmanager, node):
|
||||
self.cfm = configmanager
|
||||
self.node = node
|
||||
|
||||
def missing_host_key(self, client, hostname, key):
|
||||
fingerprint = 'sha512$' + hashlib.sha512(key.asbytes()).hexdigest()
|
||||
cfg = self.cfm.get_node_attributes(
|
||||
self.node, ('pubkeys.ssh', 'pubkeys.addpolicy'))
|
||||
if 'pubkeys.ssh' not in cfg[self.node]:
|
||||
if ('pubkeys.addpolicy' in cfg[self.node] and
|
||||
cfg[self.node]['pubkeys.addpolicy'] and
|
||||
cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
|
||||
raise cexc.PubkeyInvalid('New ssh key detected',
|
||||
key.asbytes(), fingerprint,
|
||||
'pubkeys.ssh', 'newkey')
|
||||
auditlog = log.Logger('audit')
|
||||
auditlog.log({'node': self.node, 'event': 'sshautoadd',
|
||||
'fingerprint': fingerprint})
|
||||
self.cfm.set_node_attributes(
|
||||
{self.node: {'pubkeys.ssh': fingerprint}})
|
||||
return True
|
||||
elif cfg[self.node]['pubkeys.ssh']['value'] == fingerprint:
|
||||
return True
|
||||
raise cexc.PubkeyInvalid(
|
||||
'Mismatched SSH host key detected', key.asbytes(), fingerprint,
|
||||
'pubkeys.ssh', 'mismatch'
|
||||
)
|
||||
|
||||
|
||||
class SshShell(conapi.Console):
|
||||
|
||||
def __init__(self, node, config, username='', password=''):
|
||||
self.node = node
|
||||
self.ssh = None
|
||||
self.nodeconfig = config
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.inputmode = 0 # 0 = username, 1 = password...
|
||||
|
||||
def recvdata(self):
|
||||
while self.connected:
|
||||
pendingdata = self.shell.recv(8192)
|
||||
if pendingdata == '':
|
||||
self.datacallback(conapi.ConsoleEvent.Disconnect)
|
||||
return
|
||||
self.datacallback(pendingdata)
|
||||
|
||||
def connect(self, callback):
|
||||
# for now, we just use the nodename as the presumptive ssh destination
|
||||
# TODO(jjohnson2): use a 'nodeipget' utility function for architectures
|
||||
# that would rather not use the nodename as anything but an opaque
|
||||
# identifier
|
||||
self.datacallback = callback
|
||||
if self.username is not '':
|
||||
self.logon()
|
||||
else:
|
||||
self.inputmode = 0
|
||||
callback('\r\nlogin as: ')
|
||||
return
|
||||
|
||||
def logon(self):
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(
|
||||
HostKeyHandler(self.nodeconfig, self.node))
|
||||
try:
|
||||
self.ssh.connect(self.node, username=self.username,
|
||||
password=self.password, allow_agent=False,
|
||||
look_for_keys=False)
|
||||
except paramiko.AuthenticationException:
|
||||
self.inputmode = 0
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.datacallback('\r\nlogin as: ')
|
||||
return
|
||||
except paramiko.ssh_exception.NoValidConnectionsError as e:
|
||||
self.datacallback(str(e))
|
||||
self.inputmode = 0
|
||||
self.username = ''
|
||||
self.password = ''
|
||||
self.datacallback('\r\nlogin as: ')
|
||||
return
|
||||
self.inputmode = 2
|
||||
self.connected = True
|
||||
self.shell = self.ssh.invoke_shell()
|
||||
self.rxthread = eventlet.spawn(self.recvdata)
|
||||
|
||||
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')
|
||||
lastdata = data.split('\r')[0]
|
||||
if lastdata != '':
|
||||
self.datacallback(lastdata)
|
||||
self.datacallback('\r\nEnter password: ')
|
||||
self.inputmode = 1
|
||||
elif len(data) > 0:
|
||||
# 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]
|
||||
self.datacallback('\r\n')
|
||||
self.logon()
|
||||
else:
|
||||
self.shell.sendall(data)
|
||||
|
||||
def close(self):
|
||||
if self.ssh is not None:
|
||||
self.ssh.close()
|
||||
|
||||
def create(nodes, element, configmanager, inputdata):
|
||||
if len(nodes) == 1:
|
||||
return SshShell(nodes[0], configmanager)
|
||||
@@ -0,0 +1,126 @@
|
||||
# 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 module tracks each node, tenants currently active shell sessions
|
||||
# 'ConsoleSession' objects from consoleserver are used, but with the additional
|
||||
# capacity for having a multiple of sessions per node active at a given time
|
||||
|
||||
|
||||
import confluent.consoleserver as consoleserver
|
||||
import confluent.exceptions as exc
|
||||
import confluent.messages as msg
|
||||
activesessions = {}
|
||||
|
||||
|
||||
class _ShellHandler(consoleserver.ConsoleHandler):
|
||||
_plugin_path = '/nodes/{0}/_shell/session'
|
||||
_genwatchattribs = False
|
||||
_logtobuffer = False
|
||||
|
||||
def log(self, *args, **kwargs):
|
||||
# suppress logging through proving a stub 'log' function
|
||||
return
|
||||
|
||||
def _got_disconnected(self):
|
||||
self.connectstate = 'closed'
|
||||
self._send_rcpts({'connectstate': self.connectstate})
|
||||
for session in list(self.livesessions):
|
||||
session.destroy()
|
||||
|
||||
|
||||
|
||||
|
||||
def get_sessions(tenant, node, user):
|
||||
"""Get sessionids active for node
|
||||
|
||||
Given a tenant, nodename, and user; provide an iterable of sessionids.
|
||||
Each permutation of tenant, nodename and user have a distinct set of shell
|
||||
sessions.
|
||||
|
||||
:param tenant: The tenant identifier for the current scope
|
||||
:param node: The nodename of the current scope.
|
||||
:param user: The confluent user that will 'own' the session.
|
||||
"""
|
||||
return activesessions.get((tenant, node, user), {})
|
||||
|
||||
|
||||
def get_session(tenant, node, user, sessionid):
|
||||
return activesessions.get((tenant, node, user), {}).get(sessionid, None)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ShellSession(consoleserver.ConsoleSession):
|
||||
"""Create a new socket to converse with a node shell session
|
||||
|
||||
This object provides a filehandle that can be read/written
|
||||
too in a normal fashion and the concurrency, logging, and
|
||||
event watching will all be handled seamlessly. It represents a remote
|
||||
CLI shell session.
|
||||
|
||||
: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
|
||||
:param sessionid: An optional identifier to match a running session or
|
||||
customize the name of a new session.
|
||||
"""
|
||||
|
||||
def __init__(self, node, configmanager, username, datacallback=None,
|
||||
skipreplay=False, sessionid=None):
|
||||
self.sessionid = sessionid
|
||||
self.configmanager = configmanager
|
||||
self.node = node
|
||||
super(ShellSession, self).__init__(node, configmanager, username,
|
||||
datacallback, skipreplay)
|
||||
|
||||
def connect_session(self):
|
||||
global activesessions
|
||||
tenant = self.configmanager.tenant
|
||||
if (self.configmanager.tenant, self.node) not in activesessions:
|
||||
activesessions[(tenant, self.node, self.username)] = {}
|
||||
if self.sessionid is None:
|
||||
self.sessionid = 1
|
||||
while str(self.sessionid) in activesessions[(tenant, self.node, self.username)]:
|
||||
self.sessionid += 1
|
||||
self.sessionid = str(self.sessionid)
|
||||
if self.sessionid not in activesessions[(tenant, self.node, self.username)]:
|
||||
activesessions[(tenant, self.node, self.username)][self.sessionid] = _ShellHandler(self.node, self.configmanager)
|
||||
self.conshdl = activesessions[(self.configmanager.tenant, self.node, self.username)][self.sessionid]
|
||||
|
||||
def destroy(self):
|
||||
try:
|
||||
del activesessions[(self.configmanager.tenant, self.node,
|
||||
self.username)][self.sessionid]
|
||||
except KeyError:
|
||||
pass
|
||||
super(ShellSession, self).destroy()
|
||||
|
||||
def create(nodes, element, configmanager, inputdata):
|
||||
# For creating a resource, it really has to be handled
|
||||
# in httpapi/sockapi specially, like a console.
|
||||
raise exc.InvalidArgumentException('Special client code required')
|
||||
|
||||
def retrieve(nodes, element, configmanager, inputdata):
|
||||
tenant = configmanager.tenant
|
||||
user = configmanager.current_user
|
||||
if (tenant, nodes[0], user) in activesessions:
|
||||
for sessionid in activesessions[(tenant, nodes[0], user)]:
|
||||
yield msg.ChildCollection(sessionid)
|
||||
@@ -0,0 +1,103 @@
|
||||
# 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]))
|
||||
@@ -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.
|
||||
@@ -17,9 +18,10 @@
|
||||
|
||||
# This is the socket api layer.
|
||||
# It implement unix and tls sockets
|
||||
#
|
||||
#
|
||||
|
||||
import atexit
|
||||
import errno
|
||||
import os
|
||||
import pwd
|
||||
import stat
|
||||
@@ -38,6 +40,7 @@ import confluent.config.configmanager as configmanager
|
||||
import confluent.exceptions as exc
|
||||
import confluent.log as log
|
||||
import confluent.core as pluginapi
|
||||
import confluent.shellserver as shellserver
|
||||
|
||||
|
||||
tracelog = None
|
||||
@@ -62,15 +65,23 @@ class ClientConsole(object):
|
||||
if not self.xmit:
|
||||
self.pendingdata.append(data)
|
||||
return
|
||||
tlvdata.send(self.client, data)
|
||||
send_data(self.client, data)
|
||||
|
||||
def startsending(self):
|
||||
self.xmit = True
|
||||
for datum in self.pendingdata:
|
||||
tlvdata.send(self.client, datum)
|
||||
send_data(self.client, datum)
|
||||
self.pendingdata = None
|
||||
|
||||
|
||||
def send_data(connection, data):
|
||||
try:
|
||||
tlvdata.send(connection, data)
|
||||
except IOError as ie:
|
||||
if ie.errno != errno.EPIPE:
|
||||
raise
|
||||
|
||||
|
||||
def sessionhdl(connection, authname, skipauth=False):
|
||||
# For now, trying to test the console stuff, so let's just do n4.
|
||||
authenticated = False
|
||||
@@ -78,15 +89,15 @@ def sessionhdl(connection, authname, skipauth=False):
|
||||
cfm = None
|
||||
if skipauth:
|
||||
authenticated = True
|
||||
cfm = configmanager.ConfigManager(tenant=None)
|
||||
cfm = configmanager.ConfigManager(tenant=None, username=authname)
|
||||
elif authname:
|
||||
authdata = auth.authorize(authname, element=None)
|
||||
if authdata is not None:
|
||||
cfm = authdata[1]
|
||||
authenticated = True
|
||||
tlvdata.send(connection, "Confluent -- v0 --")
|
||||
send_data(connection, "Confluent -- v0 --")
|
||||
while not authenticated: # prompt for name and passphrase
|
||||
tlvdata.send(connection, {'authpassed': 0})
|
||||
send_data(connection, {'authpassed': 0})
|
||||
response = tlvdata.recv(connection)
|
||||
authname = response['username']
|
||||
passphrase = response['password']
|
||||
@@ -101,44 +112,29 @@ def sessionhdl(connection, authname, skipauth=False):
|
||||
else:
|
||||
authenticated = True
|
||||
cfm = authdata[1]
|
||||
tlvdata.send(connection, {'authpassed': 1})
|
||||
send_data(connection, {'authpassed': 1})
|
||||
request = tlvdata.recv(connection)
|
||||
while request is not None:
|
||||
try:
|
||||
process_request(
|
||||
connection, request, cfm, authdata, authname, skipauth)
|
||||
except exc.ForbiddenRequest:
|
||||
tlvdata.send(connection, {'errorcode': 403,
|
||||
'error': 'Forbidden'})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.TargetEndpointBadCredentials:
|
||||
tlvdata.send(connection, {'errorcode': 502,
|
||||
'error': 'Bad Credentials'})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.TargetEndpointUnreachable:
|
||||
tlvdata.send(connection, {'errorcode': 504,
|
||||
'error': 'Unreachable Target'})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.NotImplementedException:
|
||||
tlvdata.send(connection, {'errorcode': 501,
|
||||
'error': 'Not Implemented'})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.NotFoundException as nfe:
|
||||
tlvdata.send(connection, {'errorcode': 404,
|
||||
'error': str(nfe)})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.InvalidArgumentException as iae:
|
||||
tlvdata.send(connection, {'errorcode': 400,
|
||||
'error': 'Bad Request - ' + str(iae)})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
except exc.ConfluentException as e:
|
||||
if ((not isinstance(e, exc.LockedCredentials)) and
|
||||
e.apierrorcode == 500):
|
||||
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
send_data(connection, {'errorcode': e.apierrorcode,
|
||||
'error': e.apierrorstr,
|
||||
'detail': e.get_error_body()})
|
||||
send_data(connection, {'_requestdone': 1})
|
||||
except SystemExit:
|
||||
sys.exit(0)
|
||||
except:
|
||||
tracelog.log(traceback.format_exc(), ltype=log.DataTypes.event,
|
||||
event=log.Events.stacktrace)
|
||||
tlvdata.send(connection, {'errorcode': 500,
|
||||
send_data(connection, {'errorcode': 500,
|
||||
'error': 'Unexpected error'})
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
send_data(connection, {'_requestdone': 1})
|
||||
request = tlvdata.recv(connection)
|
||||
|
||||
|
||||
@@ -146,91 +142,118 @@ def send_response(responses, connection):
|
||||
if responses is None:
|
||||
return
|
||||
for rsp in responses:
|
||||
tlvdata.send(connection, rsp.raw())
|
||||
tlvdata.send(connection, {'_requestdone': 1})
|
||||
send_data(connection, rsp.raw())
|
||||
send_data(connection, {'_requestdone': 1})
|
||||
|
||||
|
||||
def process_request(connection, request, cfm, authdata, authname, skipauth):
|
||||
if not isinstance(request, dict):
|
||||
raise ValueError
|
||||
raise exc.InvalidArgumentException
|
||||
operation = request['operation']
|
||||
path = request['path']
|
||||
params = request.get('parameters', None)
|
||||
params = request.get('parameters', {})
|
||||
hdlr = None
|
||||
if not skipauth:
|
||||
authdata = auth.authorize(authdata[2], path, authdata[3], operation)
|
||||
auditmsg = {
|
||||
'operation': operation,
|
||||
'user': authdata[2],
|
||||
'target': path,
|
||||
}
|
||||
if authdata[3] is not None:
|
||||
auditmsg['tenant'] = authdata[3]
|
||||
if authdata is None:
|
||||
auditmsg['allowed'] = False
|
||||
auditlog.log(auditmsg)
|
||||
raise exc.ForbiddenRequest()
|
||||
auditmsg['user'] = authdata[2]
|
||||
if authdata[3] is not None:
|
||||
auditmsg['tenant'] = authdata[3]
|
||||
auditmsg['allowed'] = True
|
||||
auditlog.log(auditmsg)
|
||||
try:
|
||||
if operation == 'start':
|
||||
elems = path.split('/')
|
||||
if elems[3] != "console":
|
||||
raise exc.InvalidArgumentException()
|
||||
node = elems[2]
|
||||
ccons = ClientConsole(connection)
|
||||
skipreplay = False
|
||||
if params and 'skipreplay' in params and params['skipreplay']:
|
||||
skipreplay = True
|
||||
consession = consoleserver.ConsoleSession(
|
||||
node=node, configmanager=cfm, username=authname,
|
||||
datacallback=ccons.sendall, skipreplay=skipreplay)
|
||||
if consession is None:
|
||||
raise Exception("TODO")
|
||||
tlvdata.send(connection, {'started': 1})
|
||||
ccons.startsending()
|
||||
bufferage = consession.get_buffer_age()
|
||||
if bufferage is not False:
|
||||
tlvdata.send(connection, {'bufferage': bufferage})
|
||||
while consession is not None:
|
||||
data = tlvdata.recv(connection)
|
||||
if type(data) == dict:
|
||||
if data['operation'] == 'stop':
|
||||
consession.destroy()
|
||||
return
|
||||
elif data['operation'] == 'break':
|
||||
consession.send_break()
|
||||
continue
|
||||
elif data['operation'] == 'reopen':
|
||||
consession.reopen()
|
||||
continue
|
||||
else:
|
||||
raise Exception("TODO")
|
||||
if not data:
|
||||
consession.destroy()
|
||||
return
|
||||
consession.write(data)
|
||||
return start_term(authname, cfm, connection, params, path)
|
||||
elif operation == 'shutdown':
|
||||
configmanager.ConfigManager.shutdown()
|
||||
else:
|
||||
hdlr = pluginapi.handle_path(path, operation, cfm, params)
|
||||
except exc.NotFoundException as e:
|
||||
tlvdata.send(connection, {"errorcode": 404,
|
||||
send_data(connection, {"errorcode": 404,
|
||||
"error": "Target not found - " + str(e)})
|
||||
tlvdata.send(connection, {"_requestdone": 1})
|
||||
send_data(connection, {"_requestdone": 1})
|
||||
except exc.InvalidArgumentException as e:
|
||||
tlvdata.send(connection, {"errorcode": 400,
|
||||
send_data(connection, {"errorcode": 400,
|
||||
"error": "Bad Request - " + str(e)})
|
||||
tlvdata.send(connection, {"_requestdone": 1})
|
||||
send_data(connection, {"_requestdone": 1})
|
||||
send_response(hdlr, connection)
|
||||
return
|
||||
|
||||
|
||||
def _tlshandler():
|
||||
def start_term(authname, cfm, connection, params, path):
|
||||
elems = path.split('/')
|
||||
if len(elems) < 4 or elems[1] != 'nodes':
|
||||
raise exc.InvalidArgumentException('Invalid path {0}'.format(path))
|
||||
node = elems[2]
|
||||
ccons = ClientConsole(connection)
|
||||
skipreplay = False
|
||||
if params and 'skipreplay' in params and params['skipreplay']:
|
||||
skipreplay = True
|
||||
if elems[3] == "console":
|
||||
consession = consoleserver.ConsoleSession(
|
||||
node=node, configmanager=cfm, username=authname,
|
||||
datacallback=ccons.sendall, skipreplay=skipreplay)
|
||||
elif len(elems) >= 6 and elems[3:5] == ['shell', 'sessions']:
|
||||
if len(elems) == 7:
|
||||
sessionid = elems[5]
|
||||
else:
|
||||
sessionid = None
|
||||
consession = shellserver.ShellSession(
|
||||
node=node, configmanager=cfm, username=authname,
|
||||
datacallback=ccons.sendall, skipreplay=skipreplay,
|
||||
sessionid=sessionid)
|
||||
|
||||
else:
|
||||
raise exc.InvalidArgumentException('Invalid path {0}'.format(path))
|
||||
|
||||
if consession is None:
|
||||
raise Exception("TODO")
|
||||
send_data(connection, {'started': 1})
|
||||
ccons.startsending()
|
||||
bufferage = consession.get_buffer_age()
|
||||
if bufferage is not False:
|
||||
send_data(connection, {'bufferage': bufferage})
|
||||
while consession is not None:
|
||||
data = tlvdata.recv(connection)
|
||||
if type(data) == dict:
|
||||
if data['operation'] == 'stop':
|
||||
consession.destroy()
|
||||
return
|
||||
elif data['operation'] == 'break':
|
||||
consession.send_break()
|
||||
continue
|
||||
elif data['operation'] == 'reopen':
|
||||
consession.reopen()
|
||||
continue
|
||||
else:
|
||||
raise Exception("TODO")
|
||||
if not data:
|
||||
consession.destroy()
|
||||
return
|
||||
consession.write(data)
|
||||
|
||||
|
||||
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)
|
||||
plainsocket.bind(('::', 13001, 0, 0))
|
||||
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.listen(5)
|
||||
while (1): # TODO: exithook
|
||||
cnn, addr = plainsocket.accept()
|
||||
@@ -257,6 +280,8 @@ def _unixdomainhandler():
|
||||
os.remove("/var/run/confluent/api.sock")
|
||||
except OSError: # if file does not exist, no big deal
|
||||
pass
|
||||
if not os.path.isdir("/var/run/confluent"):
|
||||
os.makedirs('/var/run/confluent', 0755)
|
||||
unixsocket.bind("/var/run/confluent/api.sock")
|
||||
os.chmod("/var/run/confluent/api.sock",
|
||||
stat.S_IWOTH | stat.S_IROTH | stat.S_IWGRP |
|
||||
@@ -289,14 +314,17 @@ def _unixdomainhandler():
|
||||
|
||||
|
||||
class SockApi(object):
|
||||
def __init__(self):
|
||||
def __init__(self, bindhost=None, bindport=None):
|
||||
self.tlsserver = None
|
||||
self.unixdomainserver = None
|
||||
self.bind_host = bindhost or '::'
|
||||
self.bind_port = bindport or 13001
|
||||
|
||||
def start(self):
|
||||
global auditlog
|
||||
global tracelog
|
||||
tracelog = log.Logger('trace')
|
||||
auditlog = log.Logger('audit')
|
||||
self.tlsserver = eventlet.spawn(_tlshandler)
|
||||
self.tlsserver = eventlet.spawn(
|
||||
_tlshandler, self.bind_host, self.bind_port)
|
||||
self.unixdomainserver = eventlet.spawn(_unixdomainhandler)
|
||||
|
||||
@@ -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.
|
||||
@@ -16,6 +17,9 @@
|
||||
|
||||
# Various utility functions that do not neatly fit into one category or another
|
||||
import base64
|
||||
import confluent.exceptions as cexc
|
||||
import confluent.log as log
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
|
||||
@@ -56,3 +60,39 @@ def monotonic_time():
|
||||
"""
|
||||
# for now, just support POSIX systems
|
||||
return os.times()[4]
|
||||
|
||||
class TLSCertVerifier(object):
|
||||
def __init__(self, configmanager, node, fieldname):
|
||||
self.cfm = configmanager
|
||||
self.node = node
|
||||
self.fieldname = fieldname
|
||||
|
||||
def verify_cert(self, certificate):
|
||||
fingerprint = 'sha512$' + hashlib.sha512(certificate).hexdigest()
|
||||
storedprint = self.cfm.get_node_attributes(self.node, (self.fieldname,)
|
||||
)
|
||||
if self.fieldname not in storedprint[self.node]: # no stored value, check
|
||||
# policy for next action
|
||||
newpolicy = self.cfm.get_node_attributes(self.node,
|
||||
('pubkeys.addpolicy',))
|
||||
if ('pubkeys.addpolicy' in newpolicy[self.node] and
|
||||
'value' in newpolicy[self.node]['pubkeys.addpolicy'] and
|
||||
newpolicy[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
|
||||
# manual policy means always raise unless a match is set
|
||||
# manually
|
||||
raise cexc.PubkeyInvalid('New certificate detected',
|
||||
certificate, fingerprint,
|
||||
self.fieldname, 'newkey')
|
||||
# since the policy is not manual, go ahead and add new key
|
||||
# after logging to audit log
|
||||
auditlog = log.Logger('audit')
|
||||
auditlog.log({'node': self.node, 'event': 'certautoadd',
|
||||
'fingerprint': fingerprint})
|
||||
self.cfm.set_node_attributes(
|
||||
{self.node: {self.fieldname: fingerprint}})
|
||||
return True
|
||||
elif storedprint[self.node][self.fieldname]['value'] == fingerprint:
|
||||
return True
|
||||
raise cexc.PubkeyInvalid(
|
||||
'Mismatched certificate detected', certificate, fingerprint,
|
||||
self.fieldname, 'mismatch')
|
||||
|
||||
@@ -12,7 +12,7 @@ Group: Development/Libraries
|
||||
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
|
||||
Prefix: %{_prefix}
|
||||
BuildArch: noarch
|
||||
Requires: pyghmi, eventlet, greenlet, pycrypto >= 2.6.1, confluent_client, PyPAM, pyparsing
|
||||
Requires: python-pyghmi, python-eventlet, python-greenlet, python-crypto >= 2.6.1, confluent_client, pyparsing, python-paramiko, python-dns
|
||||
Vendor: Jarrod Johnson <jjohnson2@lenovo.com>
|
||||
Url: http://xcat.sf.net/
|
||||
|
||||
@@ -33,6 +33,9 @@ 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
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
|
||||
a = Analysis(['c:/Python27/Scripts/confluentsrv.py'],
|
||||
pathex=[],
|
||||
hiddenimports=['pyghmi.constants', 'pyghmi.exceptions', 'pyghmi.ipmi.console', 'pyghmi.ipmi.private.constants', 'pyghmi.ipmi.private', 'pyghmi.ipmi.private.session', 'pyghmi.ipmi.command', 'pyghmi.ipmi.events', 'pyghmi.ipmi.fru', 'pyghmi.ipmi.private.spd', 'pyghmi.ipmi.oem.lookup', 'pyghmi.ipmi.oem.generic', 'pyghmi.ipmi.oem.lenovo', 'pyghmi.ipmi.private.util', 'pyghmi.ipmi.sdr'],
|
||||
hookspath=None,
|
||||
runtime_hooks=None,
|
||||
excludes=None,
|
||||
cipher=block_cipher)
|
||||
pyz = PYZ(a.pure,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=True,
|
||||
name='confluentsrv.exe',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
||||
coll = COLLECT(exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
Tree('confluent/plugins', prefix='confluent/plugins'),
|
||||
strip=None,
|
||||
upx=True,
|
||||
name='confluentsrv')
|
||||
@@ -1,3 +1,8 @@
|
||||
cd `dirname $0`
|
||||
VERSION=`cat VERSION`
|
||||
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`
|
||||
fi
|
||||
echo $VERSION > VERSION
|
||||
sed -e "s/#VERSION#/$VERSION/" setup.py.tmpl > setup.py
|
||||
|
||||
@@ -10,11 +10,13 @@ setup(
|
||||
description='confluent systems management server',
|
||||
packages=['confluent', 'confluent/config', 'confluent/interface',
|
||||
'confluent/plugins/hardwaremanagement/',
|
||||
'confluent/plugins/shell/',
|
||||
'confluent/plugins/configuration/'],
|
||||
install_requires=['pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
|
||||
install_requires=['paramiko', 'pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
|
||||
'pyghmi>=0.6.5'],
|
||||
scripts=['bin/confluent'],
|
||||
data_files=[('/etc/init.d', ['sysvinit/confluent']),
|
||||
('/usr/lib/systemd/system', ['systemd/confluent.service']),
|
||||
('/opt/confluent/lib/python/confluent/plugins/console/', [])],
|
||||
|
||||
)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# IBM(c) 2015 Apache 2.0
|
||||
[Unit]
|
||||
Description=Confluent hardware manager
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
PIDFile=/var/run/confluent/pid
|
||||
ExecStart=/opt/confluent/bin/confluent
|
||||
ExecStop=/opt/confluent/bin/confetty shutdown /
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -11,35 +11,56 @@
|
||||
if [ -f /etc/init.d/functions ]; then
|
||||
. /etc/init.d/functions
|
||||
LOG_SUCCESS=success
|
||||
LOG_FAILURE=failure
|
||||
elif [ -f /lib/lsb/init-functions ]; then
|
||||
. /lib/lsb/init-functions
|
||||
LOG_SUCCESS=log_success_msg
|
||||
LOG_FAILURE=log_failure_msg
|
||||
else
|
||||
echo "Unknown platform"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
confluent=/opt/confluent/bin/confluent
|
||||
confetty=/opt/confluent/bin/confetty
|
||||
|
||||
stop() {
|
||||
echo -n 'Stopping Confluent: '
|
||||
if [ -S /var/run/confluent/api.sock ]; then
|
||||
$confetty shutdown /
|
||||
fi
|
||||
$LOG_SUCCESS
|
||||
echo
|
||||
return
|
||||
}
|
||||
|
||||
start() {
|
||||
echo -n 'Starting Confluent: '
|
||||
$confluent
|
||||
if [ $? -eq 0 ]; then
|
||||
$LOG_SUCCESS
|
||||
echo
|
||||
return 0
|
||||
else
|
||||
$LOG_FAILURE
|
||||
echo
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
case $1 in
|
||||
restart)
|
||||
if [ -S /var/run/confluent/api.sock ]; then
|
||||
echo -n 'Stopping Confluent '
|
||||
/opt/confluent/bin/confetty shutdown /
|
||||
fi
|
||||
echo -n 'Starting Confluent '
|
||||
/opt/confluent/bin/confluent
|
||||
$LOG_SUCCESS
|
||||
stop
|
||||
start
|
||||
;;
|
||||
start)
|
||||
echo -n 'Starting Confluent '
|
||||
/opt/confluent/bin/confluent
|
||||
$LOG_SUCCESS
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
echo -n 'Stopping Confluent '
|
||||
if [ -S /var/run/confluent/api.sock ]; then
|
||||
/opt/confluent/bin/confetty shutdown /
|
||||
fi
|
||||
$LOG_SUCCESS
|
||||
stop
|
||||
;;
|
||||
status)
|
||||
status -p /var/run/confluent/pid $confluent
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
Reference in New Issue
Block a user