2
0
mirror of https://github.com/xcat2/confluent.git synced 2026-09-02 15:36:05 +00:00
Files
confluent/confluent_server/confluent/sshutil.py
T
Markus Hilger 9148a9e1cf Run the module self tests through asyncio
These __main__ blocks called coroutines as if they were functions, so they did
nothing at all. Single calls go through asyncio.run; sshutil, proxmox and
vcenter needed an _selftest coroutine. Two were invisible to pyrefly because
repr() and list() count as using the result: vcenter's get_vm_serial needs an
await, and proxmox's get_vm_inventory is an async generator.

lldp called _extract_neighbor_data twice, once correctly, so the bare call is
dropped. xcc3.remote_nodecfg is not a self test: every other handler defines
it as a coroutine and selfservice.py awaits it.
2026-08-10 14:36:37 +02:00

284 lines
9.2 KiB
Python

#!/usr/bin/python
import asyncio
import base64
import confluent.config.configmanager as cfm
import confluent.collective.manager as collective
import confluent.util as util
import glob
import os
import signal
import shutil
import subprocess
import tempfile
import socket
agent_pid = None
ready_keys = {}
_sshver = None
async def sshver():
global _sshver
if _sshver is None:
_, output =await util.check_output('ssh', '-V')
_sshver = float(output.split()[0].split(b'_')[1].split(b'p')[0])
return _sshver
def normalize_uid():
curruid = os.geteuid()
neededuid = os.stat('/etc/confluent').st_uid
if curruid != neededuid:
os.seteuid(neededuid)
if os.geteuid() != neededuid:
raise Exception('Need to run as root or owner of /etc/confluent')
return curruid
agent_starting = False
agent_tmpdir = None
async def assure_agent():
global agent_starting
global agent_pid
global agent_tmpdir
while agent_starting:
await asyncio.sleep(0.1)
if agent_pid is None:
try:
agent_starting = True
try:
os.makedirs('/run/confluent/ssh', mode=0o700)
except OSError as e:
if e.errno != 17:
raise
os.chmod('/run/confluent/ssh', 0o700)
agent_tmpdir = tempfile.mkdtemp(dir='/run/confluent/ssh', prefix='agent.')
os.chmod(agent_tmpdir, 0o700)
sockpath = os.path.join(agent_tmpdir, 'agent.sock')
sai = (await util.check_output(['ssh-agent', '-a', sockpath]))[0]
for line in sai.split(b'\n'):
if b';' not in line:
continue
line, _ = line.split(b';', 1)
if b'=' not in line:
continue
k, v = line.split(b'=', 1)
if not isinstance(k, str):
k = k.decode('utf8')
v = v.decode('utf8')
if k == 'SSH_AGENT_PID':
agent_pid = int(v)
os.environ[k] = v
finally:
agent_starting = False
return True
async def get_passphrase():
if await sshver() <= 7.6:
return ''
# convert the master key to base64
# for use in ssh passphrase context
if cfm._masterkey is None:
cfm.init_masterkey()
phrase = base64.b64encode(cfm._masterkey)
if not isinstance(phrase, str):
phrase = phrase.decode('utf8')
return phrase
class AlreadyExists(Exception):
pass
async def initialize_ca():
ouid = normalize_uid()
# if already there, skip, make warning
myname = collective.get_myname()
if os.path.exists('/etc/confluent/ssh/ca.pub'):
cafilename = '/var/lib/confluent/public/site/ssh/{0}.ca'.format(myname)
shutil.copy('/etc/confluent/ssh/ca.pub', cafilename)
os.seteuid(ouid)
raise AlreadyExists()
try:
os.makedirs('/etc/confluent/ssh', mode=0o700)
except OSError as e:
if e.errno != 17:
raise
finally:
os.seteuid(ouid)
comment = '{0} SSH CA'.format(myname)
await util.check_call(
'ssh-keygen', '-C', comment, '-t', 'ed25519', '-f',
'/etc/confluent/ssh/ca', '-N', await get_passphrase(),
preexec_fn=normalize_uid)
ouid = normalize_uid()
try:
try:
os.makedirs('/var/lib/confluent/public/site/ssh/', mode=0o755)
except OSError as e:
if e.errno != 17:
raise
cafilename = '/var/lib/confluent/public/site/ssh/{0}.ca'.format(myname)
shutil.copy('/etc/confluent/ssh/ca.pub', cafilename)
finally:
os.seteuid(ouid)
# newent = '@cert-authority * ' + capub.read()
def cleanup():
if agent_pid:
os.kill(int(agent_pid), signal.SIGTERM)
if agent_tmpdir:
shutil.rmtree(agent_tmpdir)
adding_key = False
async def prep_ssh_key(keyname):
global adding_key
global agent_pid
while adding_key:
await asyncio.sleep(0.1)
adding_key = True
if agent_pid:
if os.path.exists(os.environ['SSH_AUTH_SOCK']):
sock = None
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(os.environ['SSH_AUTH_SOCK'])
except Exception:
os.unlink(os.environ['SSH_AUTH_SOCK'])
os.rmdir(os.path.dirname(os.environ['SSH_AUTH_SOCK']))
finally:
if sock:
sock.close()
if not os.path.exists(os.environ['SSH_AUTH_SOCK']):
agent_pid = None
ready_keys.clear()
if keyname in ready_keys:
adding_key = False
return
if not await assure_agent():
ready_keys[keyname] = 1
adding_key = False
return
try:
os.makedirs('/run/confluent/ssh', mode=0o700)
except OSError as e:
if e.errno != 17:
raise
os.chmod('/run/confluent/ssh', 0o700)
tmpdir = tempfile.mkdtemp(dir='/run/confluent/ssh', prefix='askpass.')
try:
askpass = os.path.join(tmpdir, 'askpass.sh')
with open(askpass, 'w') as ap:
ap.write('#!/bin/sh\necho $CONFLUENT_SSH_PASSPHRASE\nrm {0}\n'.format(askpass))
os.chmod(askpass, 0o700)
os.environ['CONFLUENT_SSH_PASSPHRASE'] = await get_passphrase()
olddisplay = os.environ.get('DISPLAY', None)
oldaskpass = os.environ.get('SSH_ASKPASS', None)
os.environ['DISPLAY'] = 'NONE'
os.environ['SSH_ASKPASS'] = askpass
try:
await util.check_call('ssh-add', keyname, stdin=subprocess.DEVNULL)
finally:
del os.environ['CONFLUENT_SSH_PASSPHRASE']
del os.environ['DISPLAY']
del os.environ['SSH_ASKPASS']
if olddisplay:
os.environ['DISPLAY'] = olddisplay
if oldaskpass:
os.environ['SSH_ASKPASS'] = oldaskpass
ready_keys[keyname] = 1
finally:
adding_key = False
shutil.rmtree(tmpdir)
async def sign_host_key(pubkey, nodename, principals=()):
tmpdir = tempfile.mkdtemp()
try:
await prep_ssh_key('/etc/confluent/ssh/ca')
ready_keys['ca.pub'] = 1
pkeyname = os.path.join(tmpdir, 'hostkey.pub')
with open(pkeyname, 'wb') as pubfile:
pubfile.write(pubkey)
principals = set(principals)
principals.add(nodename)
principals = ','.join(sorted(principals))
flags = '-Us' if await sshver() > 7.6 else '-s'
keyname = '/etc/confluent/ssh/ca.pub' if flags == '-Us' else '/etc/confluent/ssh/ca'
await util.check_call(
'ssh-keygen', flags, keyname, '-I', nodename,
'-n', principals, '-h', pkeyname)
certname = pkeyname.replace('.pub', '-cert.pub')
with open(certname) as cert:
return cert.read()
finally:
shutil.rmtree(tmpdir)
async def initialize_root_key(generate, automation=False):
authorized = []
myname = collective.get_myname()
alreadyexist = False
for currkey in glob.glob('/root/.ssh/*.pub'):
authorized.append(currkey)
if generate and not authorized and not automation:
await util.check_call('ssh-keygen', '-t', 'ed25519', '-f', '/root/.ssh/id_ed25519', '-N', '')
for currkey in glob.glob('/root/.ssh/*.pub'):
authorized.append(currkey)
if automation and generate:
if os.path.exists('/etc/confluent/ssh/automation'):
alreadyexist = True
else:
ouid = normalize_uid()
try:
os.makedirs('/etc/confluent/ssh', mode=0o700)
except OSError as e:
if e.errno != 17:
raise
finally:
os.seteuid(ouid)
await util.check_call(
'ssh-keygen', '-t', 'ed25519',
'-f','/etc/confluent/ssh/automation', '-N', await get_passphrase(),
'-C', 'Confluent Automation by {}'.format(myname),
preexec_fn=normalize_uid)
authorized = ['/etc/confluent/ssh/automation.pub']
ouid = normalize_uid()
try:
os.makedirs('/var/lib/confluent/public/site/ssh', mode=0o755)
except OSError as e:
if e.errno != 17:
raise
finally:
os.seteuid(ouid)
neededuid = os.stat('/etc/confluent').st_uid
if automation:
suffix = 'automationpubkey'
else:
suffix = 'rootpubkey'
keyname = '/var/lib/confluent/public/site/ssh/{0}.{1}'.format(
myname, suffix)
if authorized:
with open(keyname, 'w'):
pass
for auth in authorized:
with open(auth, 'r') as local_key:
with open(keyname, 'a') as dest:
dest.write(local_key.read())
if os.path.exists(keyname):
os.chmod(keyname, 0o644)
os.chown(keyname, neededuid, -1)
if alreadyexist:
raise AlreadyExists()
def ca_exists():
return os.path.exists('/etc/confluent/ssh/ca')
async def _selftest():
await initialize_root_key(True)
if not ca_exists():
await initialize_ca()
print(repr(await sign_host_key(
open('/etc/ssh/ssh_host_ed25519_key.pub').read(), collective.get_myname())))
if __name__ == '__main__':
asyncio.run(_selftest())