2017-02-20 09:25:18 +01:00
#!/usr/bin/python
import os
import sys
import subprocess
from jinja2 import Template
import codecs
import argparse
__version__ = " 0.1 "
__author__ = " Adel Daouzli "
__licence__ = " GPL3 "
g_config_file = " ./vh_le.conf "
global g_verbosity
g_verbosity = False
# parse command line arguments
parser = argparse . ArgumentParser ( description = " Deploy a DNS configuration with Let ' s Encrypt configuration. Each provided option will override the config file options. " )
parser . add_argument ( ' -l ' , ' --ls-steps ' , help = ' show the list of available steps ' ,
action = " store_true " )
parser . add_argument ( ' -s ' , ' --step ' , dest = ' step ' , help = ' run a given step (e.g. 3) ' )
parser . add_argument ( ' -f ' , ' --from-step ' , dest = ' from_step ' ,
help = ' run from a given step to the end (e.g. 3) ' )
parser . add_argument ( ' -v ' , ' --verbose ' , help = ' If set will print more informations ' ,
action = " store_true " )
parser . add_argument ( ' -c ' , ' --config-file ' , dest = ' config_file ' ,
help = ' the configuration file of the script (default ./vh_le.conf) ' )
parser . add_argument ( ' -d ' , ' --main-domain ' , dest = ' main_domain ' ,
help = ' the main domain (e.g. www.hadoly.fr) ' )
parser . add_argument ( ' -t ' , ' --tld ' , dest = ' tld ' ,
help = ' list of TLDs used to determine the secondary domains (e.g. [ " fr " , " org " ]) ' )
parser . add_argument ( ' -b ' , ' --sub ' , dest = ' sub ' ,
help = ' list of subdomains used to determine the secondary domains (e.g. [ " www " ]) ' )
parser . add_argument ( ' -n ' , ' --name ' , dest = ' name ' ,
help = " domain ' s base name used to determine the secondary domains (e.g. hadoly) " )
parser . add_argument ( ' -z ' , ' --main-zone ' , dest = ' main_zone ' ,
help = " the main zone record without extension (e.g. merlin.hadoly) " )
parser . add_argument ( ' -i ' , ' --ip6-back ' , dest = ' ip6_back ' ,
help = " the IPv6 (e.g. 2001:912:3064:131::1:) " )
parser . add_argument ( ' --certif-folder ' , dest = ' certif_folder ' ,
help = " path for Let ' sEncrypt certificate (default /etc/nginx/sites) " )
parser . add_argument ( ' --acme-folder ' , dest = ' acme_folder ' ,
help = " path for ACME configuration (default /etc/acme) " )
parser . add_argument ( ' --template-vhost ' , dest = ' template_vhost ' ,
help = " the template Vhost file (default ./nginx_vhost_template) " )
parser . add_argument ( ' --template-le-vhost ' , dest = ' template_le_vhost ' ,
help = " the temporary template Vhost file for Let ' s Encrypt (default ./nginx_le_vhost_template) " )
parser . add_argument ( ' --template-acme ' , dest = ' template_acme ' ,
help = " the template file for ACME config (default ./acme_config_template) " )
parser . add_argument ( ' --sites-available ' , dest = ' sites_available ' ,
help = " path to sites-available (default /etc/nginx/sites-available) " )
parser . add_argument ( ' --sites-enabled ' , dest = ' sites_available ' ,
help = " path to sites-enabled (default /etc/nginx/sites-enabled) " )
args = parser . parse_args ( )
#g_tld = ['fr', 'org']
#g_name = "hadoly"
#g_sub = ['www']
#g_domain_main = "www.hadoly.fr"
#g_ip6_back = "2001:0912:3064:XXXX"
#g_main_zone = "merlin.hadoly.fr"
#g_template_nginx_vhost = "./nginx_vhost_template"
#g_template_le_nginx_vhost = "./nginx_le_vhost_template"
#g_template_acme_conf = "./acme_config_template"
#g_available_path = "./available" # "/etc/nginx/sites-available"
#g_enabled_path = "./enabled" # "/etc/nginx/sites-enabled"
#g_le_certificate_folder = "./sites" # "/etc/nginx/sites"
#g_acme_folder = "./acme" # "/etc/acme"
############# Functions
def print_info ( msg ) :
print ( msg )
def print_debug ( msg ) :
if g_verbosity :
print ( " DEBUG: " + str ( msg ) )
def print_error ( msg ) :
print ( " ERROR: " + str ( msg ) )
def _interpret ( value ) :
""" Interpret a value as a string (with " or ' ) or a list
: param value : the value to interpret ( should be a string )
: type : str
: return : string or a list
"""
if type ( value ) is str :
value = value . strip ( )
# string
if ( value . startswith ( " ' " ) and value . endswith ( " ' " ) ) or \
( value . startswith ( ' " ' ) and value . endswith ( ' " ' ) ) :
value = value [ 1 : - 1 ]
# list
elif value . startswith ( " [ " ) and value . endswith ( " ] " ) :
value = [ _interpret ( e ) for e in value [ 1 : - 1 ] . split ( " , " ) ]
return value
def get_config_from_file ( filename ) :
""" Read the parameters from a configuration file
: param filename : the configuration file name
: return : a dictionary with the configuration
"""
config = { }
with open ( filename ) as f :
print_debug ( " Get config from file ' {} ' " . format ( filename ) )
for line in f . readlines ( ) :
line = line . strip ( )
# ignore empty lines and comments
if line == " " or line . startswith ( " # " ) :
continue
param , value = line . split ( " = " , 1 )
param , value = param . strip ( ) , value . strip ( )
value = _interpret ( value )
config [ param ] = value
return config
def shell_command ( cmd , get_stderr = False , * * kwargs ) :
""" Run a shell command
: param cmd : the command with parameters separated by spaces
: param get_stderr : if True will get also stderr
: return : stdout or ( stdout , stderr ) if get_stderr or None if failed
"""
ret = None
try :
if get_stderr :
ret = subprocess . check_output ( cmd . split ( ' ' ) , * * kwargs )
else :
ret = subprocess . check_output ( cmd . split ( ' ' ) , stderr = subprocess . PIPE , * * kwargs )
print_debug ( " run command ' {} ' " . format ( cmd ) )
except Exception as e :
print_debug ( " failed to run command: {} \n got exception: < {} > " . format ( cmd , e ) )
return ret
def check_dns_record ( domain , expected = None , record_type = " CNAME " , resolver = None ) :
""" Check if a DNS record is correct for a domain
: param domain : the domain name to check ( e . g . " www.hadoly.fr " )
: param expected : the expected record in a tuple base name and TLDs
( e . g ( " merlin.hadoly " , [ " fr " , " org " ] ) ) or a string . If None will just
expect a record ( default None )
: param record_type : the record type ( default " CNAME " )
: param resolver : a resolver to use . If None will use the local system one
( default None )
: return : True if success else False
"""
ret = False
if resolver :
res = shell_command ( " dig +short {} {} @ {} " . format ( record_type , domain , resolver ) )
else :
res = shell_command ( " dig +short {} {} " . format ( record_type , domain ) )
if type ( expected ) is str and type ( res ) is str and res . startswith ( expected ) :
ret = True
elif type ( expected ) is tuple and type ( res ) is str :
base , tlds = expected
tlds = [ tlds ] if type ( tlds ) is str else tlds
for tld in tlds :
tld = tld [ 1 : ] if tld [ 0 ] == ' . ' else tld
if res . startswith ( base + " . " + tld ) :
print_debug ( " found DNS record : {} " . format ( res ) )
ret = True
break
elif expected is None and res :
ret = True
return ret
def generate_file ( template_file , output_file , data ) :
""" Generate a file based on a template file
: param template_file : the input template file
: param output_file : the output file to generate
: param data : the data to provide to the template
: type data : dict
: return : True if success else False
"""
ret = False
#print_debug("generate_file(template_file={}, output_file={}, data={})".format(template_file, output_file, data))
with open ( template_file ) as f :
print_debug ( " Use template ' {} ' " . format ( template_file ) )
d = f . read ( )
template = Template ( d )
content = template . render ( * * data )
with codecs . open ( output_file , " w " , ' utf-8 ' ) as fo :
fo . write ( content )
ret = True
print_debug ( " file ' {} ' generated ! " . format ( output_file ) )
return ret
################################################################################
################################################################################
################################################################################
################################################################################
### list steps and stop
steps = '''
STEP 1 : DNS records
STEP 2 : Create Nginx available configuration
STEP 3 : Create sites - enabled Nginx link
STEP 4 : Reload Nginx
STEP 5 : create Nginx folder that will receive the Let ' s Encrypt certificate
STEP 6 : create the acme configuration file
STEP 7 : acme create
STEP 8 : acme renew
STEP 9 : update the Nginx enable final configuration file
STEP 10 : reload Nginx
'''
if args . ls_steps :
print ( steps )
sys . exit ( 0 )
############### Prepare config
# read configuration from file
if args . config_file :
conf_file = args . config_file
else :
conf_file = g_config_file
c = get_config_from_file ( conf_file )
# overwrite configuration with command line args
if args . verbose :
g_verbosity = True
if args . main_domain :
c [ ' domain_main ' ] = args . main_domain
if args . tld :
c [ ' tld ' ] = args . tld
if args . sub :
c [ ' sub ' ] = args . sub
if args . name :
c [ ' name ' ] = args . name
if args . ip6_back :
c [ ' ip6_back ' ] = args . ip6_back
if args . main_zone :
c [ ' main_zone ' ] = args . main_zone
if args . certif_folder :
c [ ' le_certificate_folder ' ] = args . certif_folder
if args . acme_folder :
c [ ' acme_folder ' ] = args . acme_folder
if args . template_vhost :
c [ ' template_nginx_vhost ' ] = args . template_vhost
if args . template_le_vhost :
c [ ' template_le_nginx_vhost ' ] = args . template_le_vhost
if args . template_acme :
c [ ' template_acme_conf ' ] = args . template_acme
if args . sites_available :
c [ ' available_path ' ] = args . sites_available
if args . sites_available :
c [ ' enabled_path ' ] = args . sites_available
# build secondary domains
domains = [ ]
for tld in c [ ' tld ' ] :
domains . append ( c [ ' name ' ] + ' . ' + tld )
for sub in c [ ' sub ' ] :
domains . append ( sub + ' . ' + c [ ' name ' ] + ' . ' + tld )
# determine the steps to run
if args . step :
step = { i + 1 : False for i in range ( 10 ) }
step [ int ( args . step ) ] = True
elif args . from_step :
step = { i + 1 : False for i in range ( 10 ) }
for i in range ( 11 - int ( args . from_step ) ) :
step [ i + int ( args . from_step ) ] = True
else :
step = { i + 1 : True for i in range ( 10 ) }
############# Steps
##### DNS records (CNAME)
if step [ 1 ] :
print_info ( " \n ++++++ STEP 1 : DNS records ++++++ " )
# check each domain for DNS record
no_dns_record = [ ]
for domain in [ c [ ' domain_main ' ] ] + domains :
res = check_dns_record ( domain , expected = ( c [ ' main_zone ' ] , c [ ' tld ' ] ) )
if not res :
no_dns_record . append ( domain )
print_info ( " warning: No DNS record for ' {domain} ' " . format ( domain = domain ) )
# check if main domain had expected record else stop
if c [ ' domain_main ' ] in no_dns_record :
msg = """ No DNS record for ' {domain} ' . Please record it before
running this script . You may add in your DNS zone something like :
{ domain } CNAME { entry }
""" .format(domain=c[ ' domain_main ' ], entry=c[ ' main_zone ' ] + ' . ' + tld[0])
print_error ( msg )
sys . exit ( 1 )
print_info ( " Valid DNS record ( {} -> {} ) " . format ( c [ ' domain_main ' ] , c [ ' main_zone ' ] ) )
##### Create Nginx available configuration
if step [ 2 ] :
print_info ( " \n ++++++ STEP 2 : Create Nginx available configuration ++++++ " )
shell_command ( " sudo mkdir -p {} 2>/dev/null " . format ( c [ ' available_path ' ] ) )
if generate_file ( c [ ' template_le_nginx_vhost ' ] ,
c [ ' available_path ' ] + " / " + c [ ' domain_main ' ] ,
{ ' second_domains ' : ' ' . join ( domains ) } ) == False :
print_error ( " Failed to generate temporary Nginx VHost configuration file for LE " )
sys . exit ( 1 )
##### Create sites-enabled Nginx link
if step [ 3 ] :
print_info ( " \n ++++++ STEP 3 : Create sites-enabled Nginx link ++++++ " )
shell_command ( " ln -s {} {} " . format ( os . path . abspath ( c [ ' available_path ' ] + " / " + c [ ' domain_main ' ] ) ,
c [ ' domain_main ' ] ) ,
cwd = c [ ' enabled_path ' ] )
##### Reload Nginx
# Note: after that if a server on internet request on
# http://$domaine/.well-known/acme-challenge/ it will be challenged
if step [ 4 ] :
print_info ( " \n ++++++ STEP 4 : Reload Nginx ++++++ " )
shell_command ( " sudo service nginx restart " , get_stderr = True )
##### create Nginx folder that will receive the Let's Encrypt certificate
if step [ 5 ] :
print_info ( " \n ++++++ STEP 5 : create Nginx folder that will receive the Let ' s Encrypt certificate ++++++ " )
shell_command ( " sudo mkdir -p {} / {} " . format ( c [ ' le_certificate_folder ' ] ,
c [ ' domain_main ' ] ) )
##### create the acme configuration file
if step [ 6 ] :
print_info ( " \n ++++++ STEP 6 : create the acme configuration file ++++++ " )
shell_command ( " sudo mkdir -p {} 2>/dev/null " . format ( c [ ' acme_folder ' ] ) )
config_file = " {} / {} .conf " . format ( c [ ' acme_folder ' ] , c [ ' domain_main ' ] )
if generate_file ( c [ ' template_acme_conf ' ] , config_file ,
{ " main_domain " : c [ ' domain_main ' ] ,
" le_certificate_folder " : c [ ' le_certificate_folder ' ] ,
" second_domains " : ' ' . join ( domains ) } ) == False :
print_error ( " Failed to generate ACME configuration file " )
sys . exit ( 1 )
##### acme create
if step [ 7 ] :
print_info ( " \n ++++++ STEP 7 : acme create ++++++ " )
shell_command ( " sudo mkdir -p {} 2>/dev/null " . format ( c [ ' acme_folder ' ] ) )
cmd = " sudo /usr/local/bin/acme_create --config {} / {} .conf "
shell_command ( cmd . format ( c [ ' acme_folder ' ] , c [ ' domain_main ' ] ) , get_stderr = True )
##### acme renew
if step [ 8 ] :
print_info ( " \n ++++++ STEP 8 : acme renew ++++++ " )
shell_command ( " sudo mkdir -p {} 2>/dev/null " . format ( c [ ' acme_folder ' ] ) )
cmd = " sudo -u acme /usr/local/bin/acme_renew --config {} / {} .conf "
shell_command ( cmd . format ( c [ ' acme_folder ' ] , c [ ' domain_main ' ] ) , get_stderr = True )
##### update the Nginx enable final configuration file
if step [ 9 ] :
print_info ( " \n ++++++ STEP 9 : update the Nginx enable final configuration file ++++++ " )
if generate_file ( c [ ' template_nginx_vhost ' ] ,
2017-02-20 09:33:03 +01:00
c [ ' available_path ' ] + " / " + c [ ' domain_main ' ] ,
2017-02-20 09:25:18 +01:00
{ " main_domain " : c [ ' domain_main ' ] ,
" second_domains " : ' ' . join ( domains ) ,
" ip6_back " : c [ ' ip6_back ' ] } ) == False :
print_error ( " Failed to generate Nginx VHost configuration file " )
sys . exit ( 1 )
##### reload Nginx
if step [ 10 ] :
print_info ( " \n ++++++ STEP 10 : reload Nginx ++++++ " )
shell_command ( " sudo service nginx restart " , get_stderr = True )