diff --git a/katello_certs_tools/fileutils.py b/katello_certs_tools/fileutils.py index 5487257..c5b23e8 100644 --- a/katello_certs_tools/fileutils.py +++ b/katello_certs_tools/fileutils.py @@ -14,11 +14,8 @@ # import os -import select import shutil -import subprocess import sys -import tempfile def _file_contents_match(first, second): @@ -145,80 +142,3 @@ def rotateFile(filepath, depth=5, suffix='.', verbosity=0): # return the full filepath of the backed up file return pathNSuffix1 - - -def rhn_popen(cmd, progressCallback=None, bufferSize=16384, outputLog=None): - """ popen-like function, that accepts execvp-style arguments too (i.e. an - array of params, thus making shell escaping unnecessary) - - cmd can be either a string (like "ls -l /dev"), or an array of - arguments ["ls", "-l", "/dev"] - - Returns the command's error code, a stream with stdout's contents - and a stream with stderr's contents - - progressCallback --> progress bar twiddler - outputLog --> optional log file file object write method - """ - cmd_is_list = isinstance(cmd, list) or isinstance(cmd, tuple) - if cmd_is_list: - cmd = map(str, cmd) - c = subprocess.Popen(cmd, bufsize=0, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.PIPE, - close_fds=True, shell=(not cmd_is_list)) - - # We don't write to the child process - c.stdin.close() - - # Create two temporary streams to hold the info from stdout and stderr - child_out = tempfile.TemporaryFile(prefix='/tmp/my-popen-', mode='r+b') - child_err = tempfile.TemporaryFile(prefix='/tmp/my-popen-', mode='r+b') - - # Map the input file descriptor with the temporary (output) one - fd_mappings = [(c.stdout, child_out), (c.stderr, child_err)] - exitcode = None - count = 1 - - while 1: - # Is the child process done? - status = c.poll() - if status is not None: - if status >= 0: - # Save the exit code, we still have to read from the pipes - exitcode = status - else: - # Some signal sent to this process - if outputLog is not None: - outputLog("rhn_popen: Signal %s received\n" % (-status)) - exitcode = status - break - - fd_set = map(lambda x: x[0], fd_mappings) - readfds = select.select(fd_set, [], [])[0] - - for in_fd, out_fd in fd_mappings: - if in_fd in readfds: - # There was activity on this file descriptor - output = os.read(in_fd.fileno(), bufferSize) - if output: - # show progress - if progressCallback: - count = count + len(output) - progressCallback(count) - - if outputLog is not None: - outputLog(output) - - # write to the output buffer(s) - out_fd.write(output) - out_fd.flush() - - if exitcode is not None: - # Child process is done - break - - for f_in, f_out in fd_mappings: - f_in.close() - f_out.seek(0, 0) - - return exitcode, child_out, child_err diff --git a/katello_certs_tools/katello_ssl_tool.py b/katello_certs_tools/katello_ssl_tool.py index 7be578a..fcd7492 100644 --- a/katello_certs_tools/katello_ssl_tool.py +++ b/katello_certs_tools/katello_ssl_tool.py @@ -36,6 +36,7 @@ import glob import os import sys +import subprocess # Package imports import rpm @@ -45,10 +46,9 @@ CertExpTooLongException, InvalidCountryCodeException from katello_certs_tools.sslToolLib import KatelloSslToolException, \ - gendir, chdir, TempDir, \ - errnoGeneralError + gendir, chdir, TemporaryDirectory, disabled_rpm_macros, errnoGeneralError -from katello_certs_tools.fileutils import rotateFile, rhn_popen, cleanupAbsPath +from katello_certs_tools.fileutils import rotateFile, cleanupAbsPath from katello_certs_tools.sslToolConfig import ConfigFile, figureSerial, getOption, \ DEFS, MD, CRYPTO, \ @@ -111,14 +111,15 @@ def pathJoin(path, filename): return os.path.join(path, filename) -_workDirObj = None +def _fill_password(command, password, placeholder='pass:%s'): + cmd = command.copy() + cmd[command.index(placeholder)] = placeholder % password + return cmd -def _getWorkDir(): - global _workDirObj - if not _workDirObj: - _workDirObj = TempDir() - return _workDirObj.getdir() +def _run_command(command): + with TemporaryDirectory('-katello-certs-tools') as workdir, chdir(workdir): + return subprocess.check_output(command) def get_max_rpm_version(package_name): @@ -211,26 +212,14 @@ def genPrivateCaKey(password, d, verbosity=0, forceYN=0): except ValueError: pass - cwd = chdir(_getWorkDir()) try: - ret, out_stream, err_stream = rhn_popen(args % repr(password)) - finally: - chdir(cwd) - - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() - - if ret: + out = _run_command(args % repr(password)) + except subprocess.CalledProcessError as exception: raise GenPrivateCaKeyException("Certificate Authority private SSL " "key generation failed:\n%s\n%s" - % (out, err)) - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + % (exception.stdout, exception.stderr)) + if verbosity > 2 and out: + print("STDOUT:", out) # permissions: os.chmod(ca_key, 0o600) @@ -263,12 +252,12 @@ def genPublicCaCert(password, d, verbosity=0, forceYN=0): del d['--set-hostname'] configFile.save(d, caYN=1, verbosity=verbosity) - args = ("/usr/bin/openssl req -passin pass:%s -text -config %s " - "-new -x509 -days %s -%s -key %s -out %s" - % ('%s', repr(cleanupAbsPath(configFile.filename)), - repr(d['--cert-expiration']), - MD, repr(cleanupAbsPath(ca_key)), - repr(cleanupAbsPath(ca_cert)))) + command = [ + "/usr/bin/openssl", "req", "-passin", "pass:%s", "-text", + "-config", cleanupAbsPath(configFile.filename), + "-new", "-x509", "-days", d['--cert-expiration'], + "-%s" % MD, "-key", cleanupAbsPath(ca_key), "-out", cleanupAbsPath(ca_cert) + ] if verbosity >= 0: print("\nGenerating public CA certificate: %s" % ca_cert) @@ -277,7 +266,7 @@ def genPublicCaCert(password, d, verbosity=0, forceYN=0): '--set-org-unit', '--set-common-name', '--set-email'): print(' %s%s = "%s"' % (k, ' '*(18-len(k)), d[k])) if verbosity > 1: - print("Commandline:", args % "PASSWORD") + print("Commandline: %r", _fill_password(command, 'PASSWORD')) try: rotated = rotateFile(filepath=ca_cert, verbosity=verbosity) @@ -287,26 +276,14 @@ def genPublicCaCert(password, d, verbosity=0, forceYN=0): except ValueError: pass - cwd = chdir(_getWorkDir()) try: - ret, out_stream, err_stream = rhn_popen(args % repr(password)) - finally: - chdir(cwd) - - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() - - if ret: + out = _run_command(_fill_password(command, password)) + except subprocess.CalledProcessError as exception: raise GenPublicCaCertException("Certificate Authority public " "SSL certificate generation failed:\n%s\n" - "%s" % (out, err)) - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + "%s" % (exception.stdout, exception.stderr)) + if verbosity > 2 and out: + print("STDOUT:", out) appendOtherCACerts(d, ca_cert) @@ -329,14 +306,13 @@ def genServerKey(d, verbosity=0): server_key = os.path.join(serverKeyPairDir, os.path.basename(d['--server-key'])) - args = ("/usr/bin/openssl genrsa -out %s 4096" - % (repr(cleanupAbsPath(server_key)))) + command = ["/usr/bin/openssl", "genrsa", "-out", cleanupAbsPath(server_key), "4096"] # generate the server key if verbosity >= 0: print("\nGenerating the web server's SSL private key: %s" % server_key) if verbosity > 1: - print("Commandline:", args) + print("Commandline: %r" % command) try: rotated = rotateFile(filepath=server_key, verbosity=verbosity) @@ -346,25 +322,14 @@ def genServerKey(d, verbosity=0): except ValueError: pass - cwd = chdir(_getWorkDir()) try: - ret, out_stream, err_stream = rhn_popen(args) - finally: - chdir(cwd) - - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() - - if ret: + out = _run_command(command) + except subprocess.CalledProcessError as exception: raise GenServerKeyException("web server's SSL key generation failed:\n%s\n%s" - % (out, err)) - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + % (exception.stdout, exception.stderr)) + + if verbosity > 2 and out: + print("STDOUT:", out) # permissions: os.chmod(server_key, 0o600) @@ -414,26 +379,15 @@ def genServerCertReq(d, verbosity=0): except ValueError: pass - cwd = chdir(_getWorkDir()) try: - ret, out_stream, err_stream = rhn_popen(args) - finally: - chdir(cwd) - - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() - - if ret: + out = _run_command(args) + except subprocess.CalledProcessError as exception: raise GenServerCertReqException( - "web server's SSL certificate request generation " - "failed:\n%s\n%s" % (out, err)) - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + "web server's SSL certificate request generation " + "failed:\n%s\n%s" % (exception.stdout, exception.stderr)) + + if verbosity > 2 and out: + print("STDOUT:", out) # permissions: os.chmod(server_cert_req, 0o600) @@ -479,21 +433,26 @@ def genServerCert(password, d, verbosity=0): configFile = ConfigFile(ca_openssl_cnf) configFile.updateDir() - args = ("/usr/bin/openssl ca -extensions req_%s_x509_extensions -passin pass:%s -outdir ./ -config %s " - "-in %s -batch -cert %s -keyfile %s -startdate %s -days %s " - "-md %s -out %s" - % (purpose, - '%s', repr(cleanupAbsPath(ca_openssl_cnf)), - repr(cleanupAbsPath(server_cert_req)), - repr(cleanupAbsPath(ca_cert)), - repr(cleanupAbsPath(ca_key)), d['--startdate'], - repr(d['--cert-expiration']), MD, - repr(cleanupAbsPath(server_cert)))) + command = [ + "/usr/bin/openssl", "ca", "-batch", + "-extensions", "req_%s_x509_extensions" % purpose, + "-passin", "pass:%s", + "-outdir", "./", + "-config", cleanupAbsPath(ca_openssl_cnf), + "-in", cleanupAbsPath(server_cert_req), + "-cert", cleanupAbsPath(ca_cert), + "-keyfile", cleanupAbsPath(ca_key), + "-startdate", d['--startdate'], + "-days", d['--cert-expiration'], + "-md", MD, + "-out", cleanupAbsPath(server_cert), + ] + if verbosity >= 0: print("\nGenerating/signing web server's SSL certificate: %s" % d['--server-cert']) if verbosity > 1: - print("Commandline:", args % 'PASSWORD') + print("Commandline: %r", _fill_password(command, 'PASSWORD')) try: rotated = rotateFile(filepath=server_cert, verbosity=verbosity) if verbosity >= 0 and rotated: @@ -502,35 +461,23 @@ def genServerCert(password, d, verbosity=0): except ValueError: pass - cwd = chdir(_getWorkDir()) try: - ret, out_stream, err_stream = rhn_popen(args % repr(password)) - finally: - chdir(cwd) - - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() - - if ret: + out = _run_command(_fill_password(command, password)) + except subprocess.CalledProcessError as exception: + err = exception.stderr # signature for a mistyped CA password if "unable to load CA private key" in err \ and "error:0906A065:PEM routines:PEM_do_header:bad decrypt:pem_lib.c" in err \ and "error:06065064:digital envelope routines:EVP_DecryptFinal:bad decrypt:evp_enc.c" in err: raise GenServerCertException( - "web server's SSL certificate generation/signing " - "failed:\nDid you mistype your CA password?") - else: - raise GenServerCertException( - "web server's SSL certificate generation/signing " - "failed:\n%s\n%s" % (out, err)) + "web server's SSL certificate generation/signing failed:\n" + "Did you mistype your CA password?") - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + raise GenServerCertException("web server's SSL certificate generation/signing failed:\n" + "%s\n%s" % (exception.stdout, err)) + + if verbosity > 2 and out: + print("STDOUT:", out) # permissions: os.chmod(server_cert, 0o644) @@ -553,20 +500,6 @@ def genServerCert(password, d, verbosity=0): pass -def _disableRpmMacros(): - mac = cleanupAbsPath('~/.rpmmacros') - macTmp = cleanupAbsPath('~/RENAME_ME_BACK_PLEASE-lksjdflajsd.rpmmacros') - if os.path.exists(mac): - os.rename(mac, macTmp) - - -def _reenableRpmMacros(): - mac = cleanupAbsPath('~/.rpmmacros') - macTmp = cleanupAbsPath('~/RENAME_ME_BACK_PLEASE-lksjdflajsd.rpmmacros') - if os.path.exists(macTmp): - os.rename(macTmp, mac) - - def genCaRpm(d, verbosity=0): """ generates ssl cert RPM. """ @@ -599,24 +532,17 @@ def genCaRpm(d, verbosity=0): # build the CA certificate RPM args = [ 'katello-certs-gen-rpm', - "--name %s", - "--version %s", - "--release %s", - "--packager %s", - "--vendor %s", - "--group 'Applications/System'", - "--summary %s", - "--description %s", - os.path.join(ca_cert_path, "%s=%s") + '--name', ca_cert_rpm_name, + '--version', ver, + '--release', rel, + '--packager', d['--rpm-packager'], + '--vendor', d['--rpm-vendor'], + '--group', 'Applications/System', + '--summary', CA_CERT_RPM_SUMMARY, + '--description', CA_CERT_RPM_SUMMARY, + os.path.join(ca_cert_path, "%s=%s" % (ca_cert_name, cleanupAbsPath(ca_cert))), ] - args = " ".join(args) - - args = args % ((repr(ca_cert_rpm_name), ver, rel, repr(d['--rpm-packager']), - repr(d['--rpm-vendor']), repr(CA_CERT_RPM_SUMMARY), - repr(CA_CERT_RPM_SUMMARY), repr(ca_cert_name), - repr(cleanupAbsPath(ca_cert)))) - clientRpmName = '%s-%s-%s' % (ca_cert_rpm, ver, rel) if verbosity >= 0: print(""" @@ -626,16 +552,9 @@ def genCaRpm(d, verbosity=0): if verbosity > 1: print("Commandline:", args) - _disableRpmMacros() - cwd = chdir(d['--dir']) - try: + # TODO: fix + with chdir(d['--dir']), disabled_rpm_macros(): ret, out_stream, err_stream = rhn_popen(args) - except Exception: - chdir(cwd) - _reenableRpmMacros() - raise - chdir(cwd) - _reenableRpmMacros() out = out_stream.read().decode('utf-8') out_stream.close() @@ -717,34 +636,33 @@ def genServerRpm(d, verbosity=0): """ % d['--set-hostname'] # build the server RPM - args = [ + command = [ 'katello-certs-gen-rpm', - "--name %s --version %s --release %s --packager %s --vendor %s ", - "--group 'Applications/System' --summary %s --description %s --postun %s ", - server_cert_dir + "/private/%s:0600=%s ", - server_cert_dir + "/certs/%s=%s ", - server_cert_dir + "/certs/%s=%s " + '--name', server_rpm_name, + '--version', ver, + '--release', rel, + '--packager', d['--rpm-packager'], + '--vendor', d['--rpm-vendor'], + '--group', 'Applications/System', + '--summary', SERVER_RPM_SUMMARY, + '--description', description, + '--postun', cleanupAbsPath(postun_scriptlet), + server_cert_dir + "/private/%s:0600=%s" % (server_key_name, cleanupAbsPath(server_key)), + server_cert_dir + "/certs/%s=%s" % (server_cert_req_name, cleanupAbsPath(server_cert_req)), + server_cert_dir + "/certs/%s=%s" % (server_cert_name, cleanupAbsPath(server_cert)), ] - args = " ".join(args) - - args = args % (repr(server_rpm_name), ver, rel, repr(d['--rpm-packager']), - repr(d['--rpm-vendor']), - repr(SERVER_RPM_SUMMARY), repr(description), - repr(cleanupAbsPath(postun_scriptlet)), - repr(server_key_name), repr(cleanupAbsPath(server_key)), - repr(server_cert_req_name), repr(cleanupAbsPath(server_cert_req)), - repr(server_cert_name), repr(cleanupAbsPath(server_cert)) - ) - serverRpmName = "%s-%s-%s" % (server_rpm, ver, rel) + rpm_name = "%s-%s-%s" % (server_rpm, ver, rel) + rpm_src = '%s.src.rpm' % rpm_name + rpm_noarch = '%snoarch.rpm' % rpm_name if verbosity >= 0: print(""" Generating web server's SSL key pair/set RPM: - %s.src.rpm - %s.noarch.rpm""" % (serverRpmName, serverRpmName)) + %s + %s.noarch.rpm""" % (rpm_src, rpm_noarch)) if verbosity > 1: - print("Commandline:", args) + print("Commandline:", repr(command)) if verbosity >= 4: print('Current working directory:', os.getcwd()) @@ -752,36 +670,28 @@ def genServerRpm(d, verbosity=0): with open(postun_scriptlet, 'w') as scriptlet_fp: scriptlet_fp.write(POST_UNINSTALL_SCRIPT) - _disableRpmMacros() - cwd = chdir(serverKeyPairDir) - try: - ret, out_stream, err_stream = rhn_popen(args) - finally: - chdir(cwd) - _reenableRpmMacros() - os.unlink(postun_scriptlet) + with chdir(serverKeyPairDir), disabled_rpm_macros(): + try: + out = subprocess.check_output(command) + except subprocess.CalledProcessError as exc: + raise GenServerRpmException("web server's SSL key set RPM generation " + "failed:\n%s\n%s" % (exc.stdout, exc.stderr)) + finally: + os.unlink(postun_scriptlet) - out = out_stream.read().decode('utf-8') - out_stream.close() - err = err_stream.read().decode('utf-8') - err_stream.close() + if not os.path.exists(rpm_noarch): + raise GenServerRpmException('%s was not generated; command output: %s' % (rpm_noarch, out)) - if ret or not os.path.exists("%s.noarch.rpm" % serverRpmName): - raise GenServerRpmException("web server's SSL key set RPM generation " - "failed:\n%s\n%s" % (out, err)) - if verbosity > 2: - if out: - print("STDOUT:", out) - if err: - print("STDERR:", err) + if verbosity > 2 and out: + print("STDOUT:", out) - os.chmod('%s.noarch.rpm' % serverRpmName, 0o600) + os.chmod(rpm_noarch, 0o600) # write-out latest.txt information latest_txt = os.path.join(serverKeyPairDir, 'latest.txt') with open(latest_txt, 'w') as latest_fp: - latest_fp.write('%s.noarch.rpm\n' % os.path.basename(serverRpmName)) - latest_fp.write('%s.src.rpm\n' % os.path.basename(serverRpmName)) + latest_fp.write('%s\n' % os.path.basename(rpm_noarch)) + latest_fp.write('%s\n' % os.path.basename(rpm_src)) os.chmod(latest_txt, 0o600) if verbosity >= 0: @@ -792,7 +702,7 @@ def genServerRpm(d, verbosity=0): web server, or RHN Satellite, or RHN Proxy. Presumably %s.""" % repr(d['--set-hostname'])) - return "%s.noarch.rpm" % serverRpmName + return rpm_noarch def genServerCert_dependencies(password, d): diff --git a/katello_certs_tools/sslToolLib.py b/katello_certs_tools/sslToolLib.py index bebfeb8..90652bd 100644 --- a/katello_certs_tools/sslToolLib.py +++ b/katello_certs_tools/sslToolLib.py @@ -18,9 +18,9 @@ # $Id$ from __future__ import print_function +import contextlib import os import sys -import shutil import tempfile from katello_certs_tools.timeLib import DAY, now, secs2days, secs2years @@ -90,36 +90,73 @@ def gendir(directory): sys.exit(1) -def chdir(newdir): - "chdir with the previous cwd as the return value" - cwd = os.getcwd() - os.chdir(newdir) - return cwd - - -class TempDir: - """ temp directory class with a cleanup destructor and method """ - - _shutil = shutil # trying to hang onto shutil during garbage collection - - def __init__(self, suffix='-katello-ssl-tool'): - "create a temporary directory in /tmp" - - if suffix.find('/') != -1: - raise ValueError("suffix cannot be a path, only a name") - - # add some quick and dirty randomness to the tempfilename - s = '' - while len(s) < 10: - s = s + str(ord(os.urandom(1))) - self.path = tempfile.mkdtemp(suffix='-'+s+suffix) - - def getdir(self): - return self.path - getpath = getdir +@contextlib.contextmanager +def disabled_rpm_macros(): + directory = os.path.expanduser('~') + macros = os.path.join(directory, '.rpmmacros') + if os.path.exists(macros): + yield + else: + fd, temporary_file = tempfile.mkstemp(prefix='RENAME_ME_BACK_PLEASE', + suffix='.rpmmacros', dir=directory) + fd.close() + os.rename(macros, temporary_file) + try: + yield + finally: + os.rename(temporary_file, macros) - def __del__(self): - """ delete temporary directory when done with it """ - self._shutil.rmtree(self.path) - close = __del__ +@contextlib.contextmanager +def chdir(newdir): + "A context manager to temporarily work in another directory" + cwd = os.getcwd() + try: + os.chdir(newdir) + finally: + os.chdir(cwd) + + +try: + TemporaryDirectory = tempfile.TemporaryDirectory +except AttributeError: + # Python 3.2 introduced TemporaryDirectory but copied here for Python 2.7 + import shutil as _shutil + import warnings as _warnings + import weakref as _weakref + + class TemporaryDirectory(object): + """Create and return a temporary directory. This has the same + behavior as mkdtemp but can be used as a context manager. For + example: + + with TemporaryDirectory() as tmpdir: + ... + + Upon exiting the context, the directory and everything contained + in it are removed. + """ + + def __init__(self, suffix=None, prefix=None, dir=None): + self.name = tempfile.mkdtemp(suffix, prefix, dir) + self._finalizer = _weakref.finalize( + self, self._cleanup, self.name, + warn_message="Implicitly cleaning up {!r}".format(self)) + + @classmethod + def _cleanup(cls, name, warn_message): + _shutil.rmtree(name) + _warnings.warn(warn_message, ResourceWarning) + + def __repr__(self): + return "<{} {!r}>".format(self.__class__.__name__, self.name) + + def __enter__(self): + return self.name + + def __exit__(self, exc, value, tb): + self.cleanup() + + def cleanup(self): + if self._finalizer.detach(): + _shutil.rmtree(self.name)