2020-05-12 13:48:27 +00:00
#!/usr/bin/python2
2021-01-26 22:17:56 +00:00
__author__ = 'jjohnson2,bfinley'
2020-05-12 13:48:27 +00:00
import argparse
2020-05-21 21:07:58 +00:00
import glob
2020-05-22 15:05:35 +00:00
import os
2020-05-12 19:02:18 +00:00
import os.path
2020-08-12 12:59:25 +00:00
import pwd
2020-05-27 16:16:24 +00:00
import shutil
2020-05-12 13:48:27 +00:00
import sys
import time
2020-05-12 19:02:18 +00:00
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)
2020-07-30 19:03:36 +00:00
import confluent.collective.manager as collective
import eventlet.green.subprocess as subprocess
import confluent.selfservice as selfservice
import confluent.util as util
2020-05-12 19:02:18 +00:00
import confluent.client as client
2020-05-21 21:07:58 +00:00
import confluent.sshutil as sshutil
import confluent.certutil as certutil
try:
input = raw_input
except NameError:
pass
2020-05-12 19:02:18 +00:00
2020-06-26 16:40:07 +00:00
def emprint(txt):
if sys.stdout.isatty():
print('\x1b[1m\x1b[4m' + txt + '\x1b[0m')
else:
print(txt)
2020-05-22 15:05:35 +00:00
fnamechars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.^'
2020-05-12 13:48:27 +00:00
def main(args):
ap = argparse.ArgumentParser(description='Manage OS deployment resources')
sp = ap.add_subparsers(dest='command')
2020-05-21 21:07:58 +00:00
wiz = sp.add_parser('initialize', help='Do OS deployment preparation')
2021-03-09 18:34:00 +00:00
wiz.add_argument('-a', help='Initialize SSH access by confluent to nodes for automation such as ansible playbook execution', action='store_true')
2020-08-12 12:59:25 +00:00
wiz.add_argument('-g', help='Initialize a Genesis profile to boot systems into a rescue or staging environment', action='store_true')
2020-05-21 21:07:58 +00:00
wiz.add_argument('-u', help='Pull in root user key for node deployment', action='store_true')
wiz.add_argument('-s', help='Set up SSH CA for managing node to node ssh and known hosts', action='store_true')
2020-05-22 19:03:56 +00:00
wiz.add_argument('-k', help='Update local global known hosts file with confluent CA', action='store_true')
2020-05-21 21:07:58 +00:00
wiz.add_argument('-t', help='Generate new TLS key for HTTPS operation and register with confluent repository', action='store_true')
2020-05-22 19:03:56 +00:00
wiz.add_argument('-p', help='Copy in TFTP contents required for PXE support', action='store_true')
2020-05-21 21:07:58 +00:00
wiz.add_argument('-i', help='Interactively prompt for behaviors', action='store_true')
2020-08-26 17:04:12 +00:00
wiz.add_argument('-l', help='Set up local management node to allow login from managed nodes', action='store_true')
2020-05-12 13:53:46 +00:00
osip = sp.add_parser('import', help='Import an OS image from an ISO image')
2020-05-12 13:48:27 +00:00
osip.add_argument('imagefile', help='File to use for source of importing')
2020-08-07 18:12:13 +00:00
upb = sp.add_parser(
'updateboot',
help='Push profile.yaml of the named profile data into boot assets as appropriate')
upb.add_argument('profile', help='Profile to update boot assets')
2021-01-26 22:17:56 +00:00
osls = sp.add_parser('list', help='List OS images available for deployment')
2020-05-12 13:48:27 +00:00
cmdset = ap.parse_args()
2021-01-26 22:17:56 +00:00
if cmdset.command == 'list':
return oslist()
2020-05-12 13:53:46 +00:00
if cmdset.command == 'import':
2020-05-22 15:05:35 +00:00
return osimport(cmdset.imagefile)
2020-05-21 21:07:58 +00:00
if cmdset.command == 'initialize':
2020-05-22 15:05:35 +00:00
return initialize(cmdset)
2020-08-07 18:12:13 +00:00
if cmdset.command == 'updateboot':
return updateboot(cmdset.profile)
2020-05-22 15:05:35 +00:00
ap.print_help()
2020-05-21 21:07:58 +00:00
2020-07-30 18:44:02 +00:00
2020-08-12 12:59:25 +00:00
def initialize_genesis():
if not os.path.exists('/opt/confluent/genesis/x86_64/boot/kernel'):
emprint('Install the confluent-genesis package to have the '
'resources for a genesis profile')
return 1
hasconfluentuser = None
try:
hasconfluentuser = pwd.getpwnam('confluent')
except KeyError:
pass
pid = os.fork()
if pid:
retval = os.waitpid(pid, 0)
return retval[1]
retcode = 0
try:
if hasconfluentuser:
os.setgid(hasconfluentuser.pw_gid)
os.setuid(hasconfluentuser.pw_uid)
os.umask(0o22)
2020-08-12 13:26:04 +00:00
os.makedirs('/var/lib/confluent/public/os/genesis-x86_64/boot/efi/boot', 0o755)
2020-08-12 12:59:25 +00:00
os.makedirs('/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs', 0o755)
os.symlink('/opt/confluent/genesis/x86_64/boot/efi/boot/BOOTX64.EFI',
2020-08-12 13:26:04 +00:00
'/var/lib/confluent/public/os/genesis-x86_64/boot/efi/boot/BOOTX64.EFI')
2020-08-12 12:59:25 +00:00
os.symlink('/opt/confluent/genesis/x86_64/boot/efi/boot/grubx64.efi',
2020-08-12 13:26:04 +00:00
'/var/lib/confluent/public/os/genesis-x86_64/boot/efi/boot/grubx64.efi')
2020-08-12 12:59:25 +00:00
os.symlink('/opt/confluent/genesis/x86_64/boot/initramfs/distribution',
'/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs/distribution')
os.symlink('/var/lib/confluent/public/site/initramfs.cpio',
'/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs/site.cpio')
os.symlink('/opt/confluent/lib/osdeploy/genesis/initramfs/addons.cpio',
'/var/lib/confluent/public/os/genesis-x86_64/boot/initramfs/addons.cpio')
os.symlink('/opt/confluent/genesis/x86_64/boot/kernel',
'/var/lib/confluent/public/os/genesis-x86_64/boot/kernel')
2021-03-19 17:09:21 +00:00
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/ansible/',
'/var/lib/confluent/public/os/genesis-x86_64/ansible/')
2020-08-18 21:25:15 +00:00
shutil.copytree('/opt/confluent/lib/osdeploy/genesis/profiles/default/scripts/',
'/var/lib/confluent/public/os/genesis-x86_64/scripts/')
2020-08-12 12:59:25 +00:00
shutil.copyfile('/opt/confluent/lib/osdeploy/genesis/profiles/default/profile.yaml',
'/var/lib/confluent/public/os/genesis-x86_64/profile.yaml')
except Exception:
retcode = 1
finally:
os._exit(retcode)
2020-07-30 18:44:02 +00:00
def local_node_trust_setup():
allnodes, domain = selfservice.get_cluster_list()
myname = collective.get_myname()
myprincipals = set([myname])
2020-07-30 19:41:27 +00:00
restorecon = os.path.exists('/usr/sbin/restorecon')
2020-07-30 18:44:02 +00:00
neededlines = set([
'HostbasedAuthentication yes', 'HostbasedUsesNameFromPacketOnly yes',
'IgnoreRhosts no'])
2020-08-05 20:31:08 +00:00
if domain and not myname.endswith(domain):
2020-07-30 18:44:02 +00:00
myprincipals.add('{0}.{1}'.format(myname, domain))
2020-09-09 14:58:37 +00:00
if domain and '.' in myname and myname.endswith(domain):
2020-08-26 17:04:12 +00:00
myprincipals.add(myname.split('.')[0])
for pubkey in glob.glob('/etc/ssh/ssh_host_*_key.pub'):
2020-07-30 19:03:36 +00:00
currpubkey = open(pubkey, 'rb').read()
cert = sshutil.sign_host_key(currpubkey, myname, myprincipals)
certfile = pubkey.replace('key.pub', 'key-cert.pub')
2020-07-30 18:44:02 +00:00
neededlines.add('HostCertificate {0}'.format(certfile))
if os.path.exists(certfile):
os.unlink(certfile)
2020-07-30 19:03:36 +00:00
with open(certfile, 'w') as certout:
2020-07-30 18:44:02 +00:00
certout.write(cert)
2020-07-30 19:41:27 +00:00
if restorecon:
subprocess.check_call(['/usr/sbin/restorecon', certfile])
2020-07-30 18:44:02 +00:00
with open('/etc/ssh/sshd_config', 'r') as sshconf:
2020-07-30 19:20:53 +00:00
currconfig = sshconf.read().split('\n')
2020-07-30 18:44:02 +00:00
for conline in currconfig:
conline = conline.strip()
neededlines.discard(conline)
if neededlines:
with open('/etc/ssh/sshd_config', 'a') as cfgout:
for currline in neededlines:
cfgout.write(currline)
cfgout.write('\n')
with open('/etc/ssh/shosts.equiv', 'w') as equivout:
for node in util.natural_sort(allnodes):
equivout.write(node + '\n')
with open('/root/.shosts', 'w') as equivout:
for node in util.natural_sort(allnodes):
equivout.write(node + '\n')
2020-07-30 19:41:27 +00:00
if restorecon:
subprocess.check_call(
['/usr/sbin/restorecon',
'/etc/ssh/shosts.equiv', '/root/.shosts'])
2020-07-30 18:44:02 +00:00
2020-05-22 19:03:56 +00:00
def install_tftp_content():
2020-05-27 21:05:04 +00:00
tftplocation = None
2020-07-15 14:59:30 +00:00
candidates = ('/tftpboot', '/var/lib/tftpboot', '/srv/tftpboot', '/srv/tftp')
2020-05-27 21:05:04 +00:00
for cand in candidates:
if os.path.isdir(cand):
tftplocation = cand
break
if not tftplocation:
2020-08-28 11:18:07 +00:00
emprint('Unable to detect a directory for tftp content (check that tftp server is installed)')
return 1
2020-07-30 17:57:42 +00:00
if os.path.exists('/usr/lib/systemd/system/tftp.socket'):
2020-08-28 14:45:54 +00:00
if tftplocation == '/tftpboot':
2020-08-28 13:43:21 +00:00
emprint('/tftpboot is detected as tftp directory, will not try to automatically enable tftp, as it is presumed to be externally managed')
else:
try:
subprocess.check_call(['systemctl', 'enable', 'tftp.socket', '--now'])
print('TFTP service is enabled and running')
except Exception:
emprint('Unable to automatically enable and start tftp.socket, tftp server may already be running outside of systemd control')
2020-07-30 17:57:42 +00:00
else:
2020-08-28 13:19:38 +00:00
emprint(
'Detected {0} as tftp directory, but unable to determine tftp service, ensure that a tftp server is installed and enabled manually'.format(tftplocation))
2020-05-27 21:05:04 +00:00
tftplocation = '{0}/confluent/x86_64'.format(tftplocation)
try:
os.makedirs(tftplocation)
except OSError as e:
2020-08-27 21:06:10 +00:00
if e.errno != 17:
2020-05-27 21:05:04 +00:00
raise
2020-07-15 16:53:41 +00:00
shutil.copy('/opt/confluent/lib/ipxe/ipxe.efi', tftplocation)
shutil.copy('/opt/confluent/lib/ipxe/ipxe.kkpxe', tftplocation)
2020-05-27 21:05:04 +00:00
2020-05-22 19:03:56 +00:00
2020-05-21 21:07:58 +00:00
def initialize(cmdset):
if os.getuid() != 0:
sys.stderr.write('This command must run as root user\n')
sys.exit(1)
if cmdset.i:
didsomething = True
sys.stdout.write('Add root user key to be authorized to log into nodes (-u)? (y/n): ')
sys.stdout.flush()
cmdset.u = input().strip().lower().startswith('y')
2020-08-12 12:59:25 +00:00
sys.stdout.write('Initialize a profile to boot Genesis on target systems (a small Linux environment for rescue and staging use)? (y/n): ')
2020-08-13 19:29:45 +00:00
cmdset.g = input().strip().lower().startswith('y')
2020-05-21 21:07:58 +00:00
sys.stdout.write('Set up an SSH authority to help manage known_hosts and node to node ssh for all users (-s)? (y/n): ')
cmdset.s = input().strip().lower().startswith('y')
2020-05-22 19:03:56 +00:00
sys.stdout.write('Update global known hosts on this server to trust local CA certificates (-k)? (y/n): ')
cmdset.k = input().strip().lower().startswith('y')
2020-08-26 17:04:12 +00:00
sys.stdout.write('Allow managed nodes to ssh to this management node without a password (-l)? (y/n): ')
2020-08-03 13:34:18 +00:00
cmdset.l = input().strip().lower().startswith('y')
2020-05-22 19:03:56 +00:00
sys.stdout.write('Update tftp directory with binaries to support PXE (-p) (y/n): ')
cmdset.p = input().strip().lower().startswith('y')
2021-03-09 18:34:00 +00:00
sys.stdout.write('Initialize confluent ssh user key so confluent can execute remote automation (e.g. Ansible plays) (-a) (y/n): ')
cmdset.a = input().strip().lower().startswith('y')
2020-05-21 21:07:58 +00:00
sys.stdout.write('Generate new TLS certificates for HTTP, replacing any existing certificate (-t)? (y/n): ')
cmdset.t = input().strip().lower().startswith('y')
if not cmdset.t:
print(
'In order to use your own certificate authority, make sure '
'to put the certificate authority into '
'/var/lib/confluent/public/site/tls/ directory as a .pem file '
'as well as named (hash).0 where (hash) is the hash of the '
'subject.')
else:
didsomething = False
if cmdset.u:
2020-06-26 12:11:30 +00:00
didsomething = True
2020-05-21 21:07:58 +00:00
if not glob.glob('/root/.ssh/*.pub'):
sys.stderr.write('No user keys for root detected, it is recommended '
'to run ssh-keygen -t ed25519 to generate a user '
'key. For optimal security, a passphrase should be '
'used. ssh-agent may be used to make use of a '
'passphrase protected ssh key easier.\n')
sys.exit(1)
sshutil.initialize_root_key(False)
if cmdset.t:
didsomething = True
certutil.create_certificate()
2020-06-26 16:49:28 +00:00
if os.path.exists('/usr/lib/systemd/system/httpd.service'):
2020-06-26 18:11:50 +00:00
subprocess.check_call(['systemctl', 'try-restart', 'httpd'])
2020-06-26 16:49:28 +00:00
print('HTTP server has been restarted if it was running')
elif os.path.exists('/usr/lib/systemd/system/apache2.service'):
2020-06-26 18:11:50 +00:00
subprocess.check_call(['systemctl', 'try-restart', 'apache2'])
2020-06-26 16:49:28 +00:00
print('HTTP server has been restarted if it was running')
else:
emprint('New HTTPS certificates generated, restart the web server manually')
2020-05-22 15:05:35 +00:00
if cmdset.s:
didsomething = True
sshutil.initialize_ca()
2021-03-09 18:34:00 +00:00
if cmdset.a:
didsomething = True
sshutil.initialize_root_key(True, True)
2020-07-15 15:12:27 +00:00
if cmdset.p:
install_tftp_content()
2020-07-30 19:03:36 +00:00
if cmdset.l:
local_node_trust_setup()
2020-08-03 15:06:45 +00:00
if cmdset.k:
with open('/etc/ssh/ssh_known_hosts', 'a+b') as skh:
for cafile in glob.glob('/var/lib/confluent/public/site/ssh/*.ca'):
cacert = open(cafile, 'rb').read()
cacert = b'@cert-authority * ' + cacert
skh.write(cacert)
2020-08-12 12:59:25 +00:00
if cmdset.g:
initialize_genesis()
2020-08-27 15:43:55 +00:00
if not didsomething and (cmdset.k or cmdset.l or cmdset.g or cmdset.p):
2020-08-12 13:12:29 +00:00
if cmdset.g:
updateboot('genesis-x86_64')
2020-08-03 15:06:45 +00:00
sys.exit(0)
2020-05-21 21:07:58 +00:00
if not didsomething:
sys.stderr.write('Nothing was done, use initialize -i for '
'interactive mode, or see initialize -h for more options\n')
2020-05-22 15:05:35 +00:00
sys.exit(1)
tmpname = '/var/lib/confluent/public/site/initramfs.cpio.'
for x in bytearray(os.urandom(22)):
tmpname += fnamechars[x >> 2]
topack = []
2020-05-22 15:40:54 +00:00
opath = os.getcwd()
os.chdir('/var/lib/confluent/public/site')
2020-05-22 16:49:33 +00:00
topack.append('ssh/')
for currd, _, files in os.walk('ssh'):
2020-05-22 15:05:35 +00:00
for fname in files:
topack.append(os.path.join(currd, fname))
2020-05-22 16:49:33 +00:00
topack.append('tls/')
for currd, _, files in os.walk('tls'):
2020-05-22 15:05:35 +00:00
for fname in files:
topack.append(os.path.join(currd, fname))
with open(tmpname, 'wb') as initramfs:
packit = subprocess.Popen(['cpio', '-H', 'newc', '-o'],
stdout=initramfs, stdin=subprocess.PIPE)
for packfile in topack:
2020-05-22 15:40:54 +00:00
if not isinstance(packfile, bytes):
packfile = packfile.encode('utf8')
2020-05-22 15:05:35 +00:00
packit.stdin.write(packfile)
2020-05-22 15:40:54 +00:00
packit.stdin.write(b'\n')
2020-05-22 15:05:35 +00:00
packit.stdin.close()
res = packit.wait()
if res:
sys.stderr.write('Error occurred while packing site initramfs')
sys.exit(1)
2020-06-10 13:42:44 +00:00
os.rename(tmpname, '/var/lib/confluent/public/site/initramfs.cpio')
2020-08-12 12:59:25 +00:00
if cmdset.g:
updateboot('genesis-x86_64')
2020-06-10 14:17:54 +00:00
tmptarname = tmpname.replace('cpio', 'tgz')
2020-06-10 16:17:51 +00:00
tarcmd = ['tar', '-czf', tmptarname, 'ssh', 'tls']
2020-06-08 18:03:58 +00:00
subprocess.check_call(tarcmd)
2020-05-22 15:40:54 +00:00
os.chdir(opath)
2020-06-10 14:17:54 +00:00
os.rename(tmptarname, '/var/lib/confluent/public/site/initramfs.tgz')
2020-06-26 12:19:22 +00:00
print('Site initramfs content packed successfully')
2020-08-03 15:06:45 +00:00
2020-05-22 19:03:56 +00:00
if not os.path.exists('/etc/confluent/srvcert.pem'):
subprocess.check_call(['collective', 'gencert'])
2020-06-08 18:03:58 +00:00
# TODO: check selinux and segetbool for httpd_can_network_connect
2020-05-27 16:16:24 +00:00
# httpd available and enabled?
2020-05-22 19:27:25 +00:00
2020-05-12 13:48:27 +00:00
2020-08-07 18:12:13 +00:00
def updateboot(profilename):
c = client.Command()
for rsp in c.update('/deployment/profiles/{0}'.format(profilename),
{'updateboot': 1}):
if 'updated' in rsp:
print('Updated: {0}'.format(rsp['updated']))
else:
print(repr(rsp))
2021-01-26 22:17:56 +00:00
def oslist():
c = client.Command()
2021-01-27 13:51:03 +00:00
print("Distributions:")
2021-01-26 22:17:56 +00:00
for rsp in c.read('/deployment/distributions'):
if 'error' in rsp:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(" " + rsp['item']['href'].replace('/', ''))
2021-01-27 13:51:03 +00:00
print("")
2021-01-26 22:17:56 +00:00
2021-01-27 13:51:03 +00:00
print("Profiles:")
2021-01-26 22:17:56 +00:00
for rsp in c.read('/deployment/profiles'):
if 'error' in rsp:
sys.stderr.write(res['error'] + '\n')
exitcode = 1
else:
print(" " + rsp['item']['href'].replace('/', ''))
2021-01-27 13:51:03 +00:00
print("")
2021-01-26 22:17:56 +00:00
2020-05-12 13:48:27 +00:00
def osimport(imagefile):
c = client.Command()
2020-05-12 19:02:18 +00:00
imagefile = os.path.abspath(imagefile)
2021-02-01 14:05:15 +00:00
if c.unixdomain:
ofile = open(imagefile, 'rb')
2021-02-18 19:58:45 +00:00
try:
c.add_file(imagefile, ofile.fileno(), 'rb')
except Exception:
pass
2020-05-12 13:48:27 +00:00
importing = False
shortname = None
for rsp in c.create('/deployment/importing/', {'filename': imagefile}):
if 'target' in rsp:
importing = True
2020-05-12 18:47:00 +00:00
shortname = rsp['name']
2020-05-12 13:48:27 +00:00
print('Importing from {0} to {1}'.format(imagefile, rsp['target']))
else:
print(repr(rsp))
while importing:
for rsp in c.read('/deployment/importing/{0}'.format(shortname)):
if 'progress' in rsp:
sys.stdout.write('{0}: {1:.2f}% \r'.format(rsp['phase'],
rsp['progress']))
if rsp['phase'] == 'complete':
importing = False
sys.stdout.write('\n')
for profile in rsp['profiles']:
print('Deployment profile created: {0}'.format(profile))
sys.stdout.flush()
else:
print(repr(rsp))
time.sleep(0.5)
2020-06-18 15:11:53 +00:00
if shortname:
list(c.delete('/deployment/importing/{0}'.format(shortname)))
2020-05-12 13:48:27 +00:00
if __name__ == '__main__':
2020-07-15 14:59:30 +00:00
main(sys.argv)