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

Merge branch 'master' into discovery

This commit is contained in:
Jarrod Johnson
2017-02-07 13:45:33 -05:00
13 changed files with 436 additions and 33 deletions
+101 -13
View File
@@ -542,7 +542,7 @@ def get_session_node(shellargs):
return None
def conserver_command(filehandle, command):
def conserver_command(filehandle, localcommand):
# x - conserver has that as 'show baud', I am inclined to replace that with
# 'request exclusive'
# b - conserver has that as 'broadcast message', I'm tempted to use that
@@ -557,33 +557,121 @@ def conserver_command(filehandle, command):
# d - down a console... never used this...
# L - toggle logging
# w - who is on console
while not command:
ready, _, _ = select.select((filehandle,), (), (), 1)
if ready:
command += filehandle.read()
if command[0] == '.':
cmdlen = 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[0] == '.':
print("disconnect]\r")
quitconfetty(fullexit=consoleonly)
elif command[0] == 'o':
elif localcommand[0] == 'o':
tlvdata.send(session.connection, {'operation': 'reopen',
'path': currconsole})
print('reopen]\r')
elif command[0] == 'b':
elif localcommand[0] == 'b':
tlvdata.send(session.connection, {'operation': 'break',
'path': currconsole})
print("break sent]\r")
elif command[0] == '?':
elif localcommand[0] == 'p': # print
cmdlen += 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[1] == 'o': # off
print("powering off...")
session.simple_noderange_command(consolename, '/power/state', 'off')
print("complete]\r")
elif localcommand[1] == 's': # shutdown
print("shutting down...")
session.simple_noderange_command(consolename, '/power/state', 'shutdown')
print("complete]\r")
elif localcommand[1] == 'b': # boot
cmdlen += 1
localcommand = get_command_bytes(filehandle, localcommand, cmdlen)
if localcommand[2] == 's': # boot to setup
print("booting to setup...")
bootmode = 'uefi'
bootdev = 'setup'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == 'n': # boot to network
print("booting to network...")
bootmode = 'uefi'
bootdev = 'network'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
elif localcommand[2] == '\x0d': # boot to default
print("booting to default...")
bootmode = 'uefi'
bootdev = 'default'
rc = session.simple_noderange_command(consolename, '/boot/nextdevice', bootdev, bootmode=bootmode)
if rc:
print("Error]\r")
else:
rc = session.simple_noderange_command(consolename, '/power/state', 'boot')
if rc:
print("Error]\r")
else:
print("complete]\r")
else:
print("Unknown boot state.]\r")
else:
print("Unknown power state.]\r")
elif localcommand[0] == '?':
print("help]\r")
print(". disconnect\r")
print("b break\r")
print("o reopen\r")
print(". disconnect\r")
print("b break\r")
print("o reopen\r")
print("po power off\r")
print("ps shutdown\r")
print("pbs boot to setup\r")
print("pbn boot to network\r")
print("pb<ent> boot to default\r")
print("<cr> abort command\r")
elif command[0] == '\x0d':
elif localcommand[0] == '\x0d':
print("ignored]\r")
else: # not a command at all..
print("unknown -- use '?']\r")
def get_command_bytes(filehandle, localcommand, cmdlen):
while len(localcommand) < cmdlen:
ready, _, _ = select.select((filehandle,), (), (), 1)
if ready:
localcommand += filehandle.read()
return localcommand
def check_escape_seq(currinput, filehandle):
while conserversequence.startswith(currinput):
if currinput.startswith(conserversequence): # We have full sequence
+66
View File
@@ -0,0 +1,66 @@
#!/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
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
sys.path.append(path)
import confluent.client as client
argparser = optparse.OptionParser()
argparser.add_option('-b', '--bios', dest='biosmode',
action='store_true', default=False,
help='Request BIOS style boot (rather than UEFI)')
argparser.add_option('-p', '--persist', dest='persist', action='store_true',
default=False,
help='Request the boot device be persistent rather than '
'one time')
(options, args) = argparser.parse_args()
try:
noderange = args[0]
except IndexError:
sys.stderr.write(
'Usage: {0} <noderange> [default|cd|network|setup|hd]\n'.format(
sys.argv[0]))
sys.exit(1)
bootdev = None
if len(sys.argv) > 2:
bootdev = sys.argv[2]
if bootdev in ('net', 'pxe'):
bootdev = 'network'
session = client.Command()
exitcode = 0
if options.biosmode:
bootmode = 'bios'
else:
bootmode = 'uefi'
rc = session.simple_noderange_command(noderange, '/boot/nextdevice', bootdev,
bootmode=bootmode,
persistent=options.persist)
if rc:
sys.exit(rc)
else:
sys.exit(session.simple_noderange_command(noderange, '/power/state', 'boot'))
+1
View File
@@ -45,5 +45,6 @@ if len(sys.argv) > 2:
session = client.Command()
exitcode = 0
session.add_precede_key('oldstate')
sys.exit(
session.simple_noderange_command(noderange, '/power/state', setstate))
+10 -1
View File
@@ -44,6 +44,7 @@ def _parseserver(string):
class Command(object):
def __init__(self, server=None):
self._prevkeyname = None
self.connection = None
if server is None:
if 'CONFLUENT_HOST' in os.environ:
@@ -74,6 +75,9 @@ class Command(object):
if authdata['authpassed'] == 1:
self.authenticated = True
def add_precede_key(self, keyname):
self._prevkeyname = keyname
def handle_results(self, ikey, rc, res):
if 'error' in res:
sys.stderr.write('Error: {0}\n'.format(res['error']))
@@ -93,7 +97,12 @@ class Command(object):
else:
rc |= 1
elif ikey in res[node]:
print('{0}: {1}'.format(node, res[node][ikey]['value']))
if self._prevkeyname and self._prevkeyname in res[node]:
print('{0}: {2}->{1}'.format(
node, res[node][ikey]['value'],
res[node][self._prevkeyname]['value']))
else:
print('{0}: {1}'.format(node, res[node][ikey]['value']))
return rc
def simple_noderange_command(self, noderange, resource, input=None,
+4 -4
View File
@@ -1,4 +1,7 @@
from setuptools import setup
import os
scriptlist = ['bin/{0}'.format(d) for d in os.listdir('bin/')]
setup(
name='confluent_client',
@@ -7,9 +10,6 @@ setup(
author_email='jjohnson2@lenovo.com',
url='http://xcat.sf.net/',
packages=['confluent'],
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'],
scripts=scriptlist,
data_files=[('/etc/profile.d', ['confluent_env.sh'])],
)
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2017 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import optparse
import sys
import os
path = os.path.dirname(os.path.realpath(__file__))
path = os.path.realpath(os.path.join(path, '..', 'lib', 'python'))
if path.startswith('/opt'):
# if installed into system path, do not muck with things
sys.path.append(path)
import confluent.config.configmanager as cfm
import confluent.config.conf as conf
import confluent.main as main
argparser = optparse.OptionParser(
usage="Usage: %prog [options] [dump|restore] [path]")
argparser.add_option('-p', '--password',
help='Password to use to protect/unlock a protected dump')
argparser.add_option('-r', '--redact', action='store_true',
help='Redact potentially sensitive data rather than store')
argparser.add_option('-u', '--unprotected', action='store_true',
help='Specify that no password should be used to protect'
' the key information. Fields will be encrypted, '
'but keys.json will contain unencrypted decryption'
' keys that may be used to read the dump')
(options, args) = argparser.parse_args()
if len(args) != 2 or args[0] not in ('dump', 'restore'):
argparser.print_help()
sys.exit(1)
dumpdir = args[1]
if args[0] == 'restore':
pid = main.is_running()
if pid is not None:
print("Confluent is running, must shut down to restore db")
sys.exit(1)
cfm.restore_db_from_directory(dumpdir, options.password)
elif args[0] == 'dump':
if options.password is None and not (options.unprotected or options.redact):
print("Must indicate a password to protect or -u to opt opt of "
"secure value protection or -r to skip all protected data")
sys.exit(1)
os.umask(077)
main._initsecurity(conf.get_config())
if not os.path.exists(dumpdir):
os.makedirs(dumpdir)
cfm.dump_db_to_directory(dumpdir, options.password, options.redact)
@@ -65,6 +65,7 @@ import anydbm as dbm
import ast
import base64
import confluent.config.attributes as allattributes
import confluent.config.conf as conf
import confluent.log
import confluent.util
import confluent.exceptions as exc
@@ -128,6 +129,18 @@ def _get_protected_key(keydict, password, paramname):
raise exc.LockedCredentials("No available decryption key")
def _parse_key(keydata, password=None):
if keydata.startswith('*unencrypted:'):
return base64.b64decode(keydata[13:])
elif password:
salt, iv, crypt, hmac = [base64.b64decode(x)
for x in keydata.split('!')]
privkey, integkey = _derive_keys(password, salt)
return decrypt_value([iv, crypt, hmac], privkey, integkey)
raise(exc.LockedCredentials(
"Passphrase protected secret requires password"))
def _format_key(key, password=None):
if password is not None:
salt = os.urandom(32)
@@ -707,6 +720,8 @@ class ConfigManager(object):
:param uid: Custom identifier number if desired. Defaults to random.
:param displayname: Optional long format name for UI consumption
"""
if 'idmap' not in _cfgstore['main']:
_cfgstore['main']['idmap'] = {}
if uid is None:
uid = _generate_new_id()
else:
@@ -720,8 +735,6 @@ class ConfigManager(object):
self._cfgstore['users'][name] = {'id': uid}
if displayname is not None:
self._cfgstore['users'][name]['displayname'] = displayname
if 'idmap' not in _cfgstore['main']:
_cfgstore['main']['idmap'] = {}
_cfgstore['main']['idmap'][uid] = {
'tenant': self.tenant,
'username': name
@@ -1187,6 +1200,70 @@ class ConfigManager(object):
self._bg_sync_to_file()
#TODO: wait for synchronization to suceed/fail??)
def _load_from_json(self, jsondata):
"""Load fresh configuration data from jsondata
:param jsondata: String of jsondata
:return:
"""
dumpdata = json.loads(jsondata)
tmpconfig = {}
for confarea in _config_areas:
if confarea not in dumpdata:
continue
tmpconfig[confarea] = {}
for element in dumpdata[confarea]:
newelement = copy.deepcopy(dumpdata[confarea][element])
for attribute in dumpdata[confarea][element]:
if newelement[attribute] == '*REDACTED*':
raise Exception(
"Unable to restore from redacted backup")
elif attribute == 'cryptpass':
passparts = newelement[attribute].split('!')
newelement[attribute] = tuple([base64.b64decode(x)
for x in passparts])
elif 'cryptvalue' in newelement[attribute]:
bincrypt = newelement[attribute]['cryptvalue']
bincrypt = tuple([base64.b64decode(x)
for x in bincrypt.split('!')])
newelement[attribute]['cryptvalue'] = bincrypt
elif attribute in ('nodes', '_expressionkeys'):
# A group with nodes
# delete it and defer until nodes are being added
# which will implicitly fill this up
# Or _expressionkeys attribute, which will similarly
# be rebuilt
del newelement[attribute]
tmpconfig[confarea][element] = newelement
# We made it through above section without an exception, go ahead and
# replace
# Start by erasing the dbm files if present
for confarea in _config_areas:
try:
os.unlink(os.path.join(self._cfgdir, confarea))
except OSError as e:
if e.errno == 2:
pass
# Now we have to iterate through each fixed up element, using the
# set attribute to flesh out inheritence and expressions
for confarea in _config_areas:
if confarea not in tmpconfig:
continue
if confarea == 'nodes':
self.set_node_attributes(tmpconfig[confarea], True)
elif confarea == 'nodegroups':
self.set_group_attributes(tmpconfig[confarea], True)
elif confarea == 'users':
for user in tmpconfig[confarea]:
uid = tmpconfig[confarea].get('id', None)
displayname = tmpconfig[confarea].get('displayname', None)
self.create_user(user, uid=uid, displayname=displayname)
if 'cryptpass' in tmpconfig[confarea][user]:
self._cfgstore['users'][user]['cryptpass'] = \
tmpconfig[confarea][user]['cryptpass']
_mark_dirtykey('users', user, self.tenant)
self._bg_sync_to_file()
def _dump_to_json(self, redact=None):
"""Dump the configuration in json form to output
@@ -1344,28 +1421,80 @@ class ConfigManager(object):
self._recalculate_expressions(cfgobj[key], formatter, node,
changeset)
def _restore_keys(jsond, password, newpassword=None):
# the jsond from the restored file, password (if any) used to protect
# the file, and newpassword to use, (also check the service.cfg file)
global _masterkey
global _masterintegritykey
keydata = json.loads(jsond)
cryptkey = _parse_key(keydata['cryptkey'], password)
integritykey = _parse_key(keydata['integritykey'], password)
conf.init_config()
cfg = conf.get_config()
if cfg.has_option('security', 'externalcfgkey'):
keyfilename = cfg.get('security', 'externalcfgkey')
with open(keyfilename, 'r') as keyfile:
newpassword = keyfile.read()
set_global('master_privacy_key', _format_key(cryptkey,
password=newpassword))
set_global('master_integrity_key', _format_key(integritykey,
password=newpassword))
_masterkey = cryptkey
_masterintegritykey = integritykey
ConfigManager.wait_for_sync()
# At this point, we should have the key situation all sorted
def _dump_keys(password):
if _masterkey is None or _masterintegritykey is None:
init_masterkey()
cryptkey = _format_key(_masterkey, password=password)
cryptkey = '!'.join(map(base64.b64encode, cryptkey['passphraseprotected']))
if 'passphraseprotected' in cryptkey:
cryptkey = '!'.join(map(base64.b64encode,
cryptkey['passphraseprotected']))
else:
cryptkey = '*unencrypted:{0}'.format(base64.b64encode(
cryptkey['unencryptedvalue']))
integritykey = _format_key(_masterintegritykey, password=password)
integritykey = '!'.join(map(base64.b64encode, integritykey['passphraseprotected']))
if 'passphraseprotected' in integritykey:
integritykey = '!'.join(map(base64.b64encode,
integritykey['passphraseprotected']))
else:
integritykey = '*unencrypted:{0}'.format(base64.b64encode(
integritykey['unencryptedvalue']))
return json.dumps({'cryptkey': cryptkey, 'integritykey': integritykey},
sort_keys=True, indent=4, separators=(',', ': '))
def restore_db_from_directory(location, password):
try:
with open(os.path.join(location, 'keys.json'), 'r') as cfgfile:
keydata = cfgfile.read()
json.loads(keydata)
_restore_keys(keydata, password)
except IOError as e:
if e.errno == 2:
raise Exception("Cannot restore without keys, this may be a "
"redacted dump")
with open(os.path.join(location, 'main.json'), 'r') as cfgfile:
cfgdata = cfgfile.read()
ConfigManager(tenant=None)._load_from_json(cfgdata)
def dump_db_to_directory(location, password, redact=None):
with open(os.path.join(location, 'keys.json'), 'w') as cfgfile:
cfgfile.write(_dump_keys(password))
cfgfile.write('\n')
if not redact:
with open(os.path.join(location, 'keys.json'), 'w') as cfgfile:
cfgfile.write(_dump_keys(password))
cfgfile.write('\n')
with open(os.path.join(location, 'main.json'), 'w') as cfgfile:
cfgfile.write(ConfigManager(tenant=None)._dump_to_json(redact=redact))
cfgfile.write('\n')
try:
for tenant in os.listdir(
os.path.join(ConfigManager._cfgdir, '/tenants/')):
with open(os.path.join(location, tenant + '.json'), 'w') as cfgfile:
with open(os.path.join(location, 'tenants', tenant,
'main.json'), 'w') as cfgfile:
cfgfile.write(ConfigManager(tenant=tenant)._dump_to_json(
redact=redact))
cfgfile.write('\n')
+20
View File
@@ -89,6 +89,26 @@ def _updatepidfile():
pidfile.close()
def is_running():
# Utility function for utilities to check if confluent is running
try:
pidfile = open('/var/run/confluent/pid', 'r+')
fcntl.flock(pidfile, fcntl.LOCK_SH)
pid = pidfile.read()
if pid != '':
try:
os.kill(int(pid), 0)
return pid
except OSError:
# There is no process running by that pid, must be stale
pass
fcntl.flock(pidfile, fcntl.LOCK_UN)
pidfile.close()
except IOError:
pass
return None
def _checkpidfile():
try:
pidfile = open('/var/run/confluent/pid', 'r+')
+5
View File
@@ -797,6 +797,11 @@ class PowerState(ConfluentChoiceMessage):
])
keyname = 'state'
def __init__(self, node, state, oldstate=None):
super(PowerState, self).__init__(node, state)
if oldstate is not None:
self.kvpairs[node]['oldstate'] = {'value': oldstate}
class BMCReset(ConfluentChoiceMessage):
valid_values = set([
@@ -795,10 +795,22 @@ class IpmiHandler(object):
return
elif 'update' == self.op:
powerstate = self.inputdata.powerstate(self.node)
oldpower = None
if powerstate == 'boot':
oldpower = self.ipmicmd.get_power()
if 'powerstate' in oldpower:
oldpower = oldpower['powerstate']
self.ipmicmd.set_power(powerstate, wait=30)
power = self.ipmicmd.get_power()
if powerstate == 'boot' and oldpower == 'on':
power = {'powerstate': 'reset'}
else:
power = self.ipmicmd.get_power()
if powerstate == 'reset' and power['powerstate'] == 'on':
power['powerstate'] = 'reset'
self.output.put(msg.PowerState(node=self.node,
state=power['powerstate']))
state=power['powerstate'],
oldstate=oldpower))
return
def handle_reset(self):
+6 -3
View File
@@ -170,7 +170,8 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
auditlog.log(auditmsg)
try:
if operation == 'start':
return start_term(authname, cfm, connection, params, path)
return start_term(authname, cfm, connection, params, path,
authdata, skipauth)
elif operation == 'shutdown':
configmanager.ConfigManager.shutdown()
else:
@@ -187,7 +188,7 @@ def process_request(connection, request, cfm, authdata, authname, skipauth):
return
def start_term(authname, cfm, connection, params, path):
def start_term(authname, cfm, connection, params, path, authdata, skipauth):
elems = path.split('/')
if len(elems) < 4 or elems[1] != 'nodes':
raise exc.InvalidArgumentException('Invalid path {0}'.format(path))
@@ -233,7 +234,9 @@ def start_term(authname, cfm, connection, params, path):
consession.reopen()
continue
else:
raise Exception("TODO")
process_request(connection, data, cfm, authdata, authname,
skipauth)
continue
if not data:
consession.destroy()
return
@@ -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
+2 -2
View File
@@ -5,7 +5,7 @@ setup(
name='confluent_server',
version='#VERSION#',
author='Jarrod Johnson',
author_email='jbjohnso@us.ibm.com',
author_email='jjohnson2@lenovo.com',
url='http://xcat.sf.net/',
description='confluent systems management server',
packages=['confluent', 'confluent/config', 'confluent/interface',
@@ -14,7 +14,7 @@ setup(
'confluent/plugins/configuration/'],
install_requires=['paramiko', 'pycrypto>=2.6', 'confluent_client>=0.1.0', 'eventlet',
'pyghmi>=0.6.5'],
scripts=['bin/confluent'],
scripts=['bin/confluent', 'bin/confluentdbutil'],
data_files=[('/etc/init.d', ['sysvinit/confluent']),
('/usr/lib/systemd/system', ['systemd/confluent.service']),
('/opt/confluent/lib/python/confluent/plugins/console/', [])],