Skip to content
Snippets Groups Projects
geant_acme.py 5.99 KiB
#!/usr/bin/env python3
"""Geant Acme

Usage:
  geant_acme.py --domain <DOMAIN>... (--client <CLIENT>... | --wildcard) [--verbose]
  geant_acme.py (-h | --help)

Options:
  -h --help                   Show this screen.
  -c CLIENT --client=CLIENT   Client
  -d DOMAIN --domain=DOMAIN   Domain
  -w --wildcard               Use wildcard
  -v --verbose                Print out messages
"""
import os
import time
import datetime
import subprocess as sp
import configparser
from docopt import docopt
import requests

BASE_URL = 'https://infoblox.geant.org/wapi/v2.6.1'


def os_exit():
    """ exit """
    print('+' + 72*'-' + '+')
    # close logging
    end_date = time_log()
    print('[{}] Job ended'.format(end_date))
    print('+' + 72*'=' + '+')
    os.sys.exit()


def time_log():
    """ return day and time """
    return datetime.datetime.now().strftime("%d %b %Y %H:%M:%S")


def get_reference(iblox_domain, iblox_user, iblox_pw):
    """ grab reference for txt object """
    ref_obj = requests.get(
        '{}/record:txt?name=_acme-challenge.{}&_return_as_object=1'.format(
            BASE_URL, iblox_domain),
        auth=(iblox_user, iblox_pw)
    )

    if ref_obj.status_code != 200:
        return 'error'

    try:
        ref = ref_obj.json()['result'][0]['_ref']
    except IndexError:
        ref = None

    if ref:
        print('got reference object: {}'.format(ref))
    else:
        print('{} empty reference object: no challenge to delete'.format(
            iblox_domain))
    print('+' + 72*'-' + '+')

    return ref


def delete_challenge(object_reference, iblox_user, iblox_pw):
    """ delete txt record """
    del_req = requests.delete(
        '{}/{}'.format(BASE_URL, object_reference),
        auth=(iblox_user, iblox_pw)
    )
    print('delete challenge - http code (200 expected): {}'.format(del_req.status_code))
    print('+' + 72*'-' + '+')

    return del_req.status_code


def create_challenge(iblox_domain, acme_token, iblox_user, iblox_pw):
    """ upload txt record """
    print('+' + 72*'-' + '+')
    print('creating challenge _acme-challenge.{}'.format(iblox_domain))

    post_req = requests.post(
        '{}/record:txt'.format(BASE_URL),
        auth=(iblox_user, iblox_pw),
        data={
            'name': '_acme-challenge.{}'.format(iblox_domain),
            'text': acme_token,
            'ttl': '30',
            "view": "External"
        }
    )
    print('create challenge - http code (201 expected): {} {}'.format(
        post_req.status_code, post_req.reason))
    print('+' + 72*'-' + '+')

    return post_req.status_code


def run_certbot(cbot_domain, wild_card=None):
    """ get certificate from letsencrypt """
    if wild_card:
        domain_list = '*.{}'.format(cbot_domain)
    else:
        domain_list = ' -d '.join(list(reversed(cbot_domain)))

    cbot_cmd = '/usr/local/bin/certbot certonly -c /etc/letsencrypt/cli.ini' \
               + ' --manual-auth-hook=/root/bin/infoblox_hook.py' \
               + ' --cert-name {} -d {}'.format(cbot_domain[0], domain_list)

    print('running: {}'.format(cbot_cmd))

    cbot_child = sp.Popen(cbot_cmd, stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
    cbot_out, cbot_err = cbot_child.communicate()
    if cbot_child.returncode != 0:
        msg = "error running certbot: {}".format(cbot_err.decode("utf-8"))
    else:
        decoded_msg = cbot_out.decode("utf-8")
        msg = decoded_msg[:decoded_msg.rfind('\n')]

    print('\n{}'.format(msg))

    return msg


# Here we Go.
if __name__ == "__main__":

    ARGS = docopt(__doc__)
    DOMAIN = ARGS['--domain']
    CLIENTS = ARGS['--client']
    WILDCARD = ARGS['--wildcard']
    VERBOSE = ARGS['--verbose']
    if not VERBOSE:
        os.sys.stdout = open('/var/log/acme/acme.log', 'a')

    # start logging
    START_DATE = time_log()
    CMD_LINE = ' '.join(os.sys.argv)
    print('+' + 72*'=' + '+')
    print('[{}] Jost started: {}'.format(START_DATE, CMD_LINE))

    CONFIG = configparser.RawConfigParser()
    CONFIG.read_file(open('/root/.geant_acme.ini'))
    IBLOX_PASS = CONFIG.get('geant_acme', 'iblox_pass')
    IBLOX_USER = CONFIG.get('geant_acme', 'iblox_user')

    if WILDCARD:
        REAL_DOMAIN = 'domain *.{}'.format(DOMAIN[0])
    else:
        REAL_DOMAIN = 'host {}'.format(DOMAIN[0])

    print('+' + 72*'-' + '+')

    # Maybe there is an Acme challenge left over. We try to delete it first.
    for domain_item in DOMAIN:
        REF_OBJ = get_reference(domain_item, IBLOX_USER, IBLOX_PASS)
        if REF_OBJ == 'error':
            print('error retrieving reference object')
            os_exit()
        if REF_OBJ:
            DEL_STATUS = delete_challenge(
                REF_OBJ, IBLOX_USER, IBLOX_PASS)
            if DEL_STATUS != 200:
                print('{}: {} error deleting challenge on Infoblox'.format(
                    domain_item, DEL_STATUS))
                os_exit()

    # run certbot
    run_certbot(DOMAIN, WILDCARD)
    print('sleep 10 seconds to wait for DNS to settle down')
    print('+' + 72*'-' + '+')
    time.sleep(10)

    # remove the Acme challenges from Infoblox
    for domain_item in DOMAIN:
        REF_OBJ = get_reference(domain_item, IBLOX_USER, IBLOX_PASS)
        if REF_OBJ == 'error':
            print('error retrieving reference object')
            os_exit()
        if REF_OBJ:
            DEL_STATUS = delete_challenge(
                REF_OBJ, IBLOX_USER, IBLOX_PASS)
            if DEL_STATUS != 200:
                print('{}: {} error deleting challenge on Infoblox'.format(
                    domain_item, DEL_STATUS))
                os_exit()

    # if we are here, everything went fine and we can upload the certificates
    os.sys.stdout.flush()
    if WILDCARD:
        UPLOADER = '/root/bin/upload_wildcards.py'
        if VERBOSE:
            UPLOADER += ' -v'
        os.system(UPLOADER)
    else:
        for client in CLIENTS:
            UPLOADER = '/root/bin/geant_acme_uploader.py -d {} -c {}'.format(
                DOMAIN[0], client)
            if VERBOSE:
                UPLOADER += ' -v'
            os.system(UPLOADER)

    os_exit()