Files
certbot-certbot-2/certbot-ci/certbot_integration_tests/utils/misc.py
T

175 lines
5.6 KiB
Python

"""
Misc module contains stateless functions that could be used during pytest execution,
or outside during setup/teardown of the integration tests environment.
"""
import contextlib
import errno
import multiprocessing
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import time
from distutils.version import LooseVersion
import requests
from OpenSSL import crypto
from six.moves import socketserver, SimpleHTTPServer
def check_until_timeout(url):
"""
Wait and block until given url responds with status 200, or raise an exception
after 150 attempts.
:param str url: the URL to test
:raise ValueError: exception raised after 150 unsuccessful attempts to reach the URL
"""
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
for _ in range(0, 150):
time.sleep(1)
try:
if requests.get(url, verify=False).status_code == 200:
return
except requests.exceptions.ConnectionError:
pass
raise ValueError('Error, url did not respond after 150 attempts: {0}'.format(url))
class GracefulTCPServer(socketserver.TCPServer):
"""
This subclass of TCPServer allows to gracefully reuse an address that has
just been released by another instance of TCPServer.
"""
allow_reuse_address = True
@contextlib.contextmanager
def create_http_server(port):
"""
Setup and start an HTTP server for the given TCP port.
This server stay active for the lifetime of the context, and is automatically
stopped with context exit, while its temporary webroot is deleted.
:param int port: the TCP port to use
:return str: the temporary webroot attached to this server
"""
current_cwd = os.getcwd()
webroot = tempfile.mkdtemp()
def run():
GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
process = multiprocessing.Process(target=run)
try:
try:
os.chdir(webroot)
process.start()
finally:
os.chdir(current_cwd)
check_until_timeout('http://localhost:{0}/'.format(port))
yield webroot
finally:
try:
if process.is_alive():
process.terminate()
process.join() # Block until process is effectively terminated
finally:
shutil.rmtree(webroot)
def list_renewal_hooks_dirs(config_dir):
"""
Find and return path of all hooks directory for the given certbot config directory
:param str config_dir: path to the certbot config directory
:return str[]: list of path to the standard hooks directory for this certbot instance
"""
renewal_hooks_root = os.path.join(config_dir, 'renewal-hooks')
return [os.path.join(renewal_hooks_root, item) for item in ['pre', 'deploy', 'post']]
def generate_test_file_hooks(config_dir, hook_probe):
"""
Create a suite of certbot hooks scripts and put them in the relevant hooks directory
for the given certbot configuration directory. These scripts, when executed, will write
specific verbs in the given hook_probe file to allow asserting they have effectively
been executed.
:param str config_dir: current certbot config directory
:param hook_probe: path to the hook probe to test hook scripts execution
"""
if sys.platform == 'win32':
extension = 'bat'
else:
extension = 'sh'
renewal_hooks_dirs = list_renewal_hooks_dirs(config_dir)
for hook_dir in renewal_hooks_dirs:
try:
os.makedirs(hook_dir)
except OSError as error:
if error.errno != errno.EEXIST:
raise
hook_path = os.path.join(hook_dir, 'hook.{0}'.format(extension))
if extension == 'sh':
data = '''\
#!/bin/bash -xe
if [ "$0" == "{0}" ]; then
if [ -z "$RENEWED_DOMAINS" -o -z "$RENEWED_LINEAGE" ]; then
echo "Environment variables not properly set!" >&2
exit 1
fi
fi
echo $(basename $(dirname "$0")) >> "{1}"\
'''.format(hook_path, hook_probe)
else:
# TODO: Write the equivalent bat file for Windows
data = '''\
'''
with open(hook_path, 'w') as file:
file.write(data)
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC)
def manual_http_hooks(http_server_root):
"""
Generate suitable http-01 hooks command for test purpose in the given HTTP
server webroot directory.
:param str http_server_root: path to the HTTP server configured to serve http-01 challenges
:return (str, str): a tuple containing the authentication hook and the cleanup hook
"""
auth = (
'{0} -c "import os; '
"challenge_dir = os.path.join('{1}', '.well-known', 'acme-challenge'); "
'os.makedirs(challenge_dir); '
"challenge_file = os.path.join(challenge_dir, os.environ.get('CERTBOT_TOKEN')); "
"open(challenge_file, 'w').write(os.environ.get('CERTBOT_VALIDATION')); "
'"'
).format(sys.executable, http_server_root)
cleanup = (
'{0} -c "import os; import shutil; '
"well_known = os.path.join('{1}', '.well-known'); "
'shutil.rmtree(well_known); '
'"'
).format(sys.executable, http_server_root)
return auth, cleanup
def get_certbot_version():
"""
Find the version of the certbot available in PATH.
:return str: the certbot version
"""
output = subprocess.check_output(['certbot', '--version'], universal_newlines=True)
# Typical response is: output = 'certbot 0.31.0.dev0'
version_str = output.split(' ')[1].strip()
return LooseVersion(version_str)