#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Make Tunnelbox Configuration
# Copyright (C) 2012-2024 by Thomas Dreibholz
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: dreibh@simula.no

import sys
import re
import shlex
import subprocess
import base64
import hashlib

# NorNet
from NorNetTools         import *
from NorNetConfiguration import *
from NorNetAPI           import *
from NorNetNodeSetup     import *



# ###### Get local node configuration string ################################
def getLocalNodeConfigurationString(nodeIndex):
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_Node' + str(nodeIndex)])
   except:
      return ''


# ###### Get local node aliases string ######################################
def getLocalNodeAliasesString(nodeIndex):
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_Alias' + str(nodeIndex)])
   except:
      return ''


# ###### Get local node SSH keys string #####################################
def getLocalNodeSSHKeysString(nodeIndex):
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_SSHKeys' + str(nodeIndex)])
   except:
      return ''


# ###### Get DHCPD node configuration string ################################
def getLocalSiteDHCPServerDynamicConfigurationString():
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DHCPServer_Dynamic'])
   except:
      return ''


# ###### Get DHCPD node configuration string ################################
def getLocalSiteDHCPServerDynamicNodeNamesString():
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DHCPServer_NodeNames'])
   except:
      return ''


# ###### Get DHCPD node configuration string ################################
def getLocalSiteDHCPServerStaticConfigurationString(nodeIndex):
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DHCPServer_Static' + str(nodeIndex)])
   except:
      return ''


# ###### Get DHCPD node configuration string ################################
def getLocalSiteDHCPServerDenyString():
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DHCPServer_Deny'])
   except:
      return ''


# ###### Get DHCPD node configuration string ################################
def getLocalSiteRADVDConfigurationString():
   return checkParameter('NorNet_LocalSite_RADVDServer', [ 'on', 'off'])


# ###### Get DNS forwarders #################################################
def getLocalSiteDNSForwardersString():
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DNS_Forwarders'])
   except:
      return ''


# ###### Get DNS DNSSEC validation ##########################################
def getLocalSiteDNSValidateDNSSEC():
   try:
      return checkParameter('NorNet_LocalSite_DNS_ValidateDNSSEC', [ 'on', 'off'])
   except:
      return 'on'


# ###### Get Squid memory cache #############################################
def getSquidMemoryCacheSize():
   try:
      return int(NorNet_Configuration['NorNet_Squid_MemoryCache'])
   except:
      return 0


# ###### Get Squid maximum object size ######################################
def getSquidMaxObjectSize():
   try:
      return int(NorNet_Configuration['NorNet_Squid_MaxObjectSize'])
   except:
      return 0


# ###### Get Squid disk cache ###############################################
def getSquidDiskCacheSize():
   try:
      return int(NorNet_Configuration['NorNet_Squid_DiskCache'])
   except:
      return 0


# ###### Get Squid parent cache #############################################
def getSquidParentCache():
   try:
      return NorNet_Configuration['NorNet_Squid_ParentCache']
   except:
      return ''


# ###### Get Squid parent cache #############################################
def getSquidAdditionalNetworks():
   try:
      return NorNet_Configuration['NorNet_Squid_AdditionalNetworks']
   except:
      return ''


# ###### Get Squid parent cache #############################################
def getSquidTransparentProxy():
   return checkParameter('NorNet_Squid_TransparentProxy', [ 'on', 'off'])


# ###### Get tunnel configuration for default tunnel to central site ########
def getDefaultTunnel(fullSiteList, localSite, version):
   localSiteProviders          = getNorNetProvidersForSite(localSite)
   localDefaultProviderIndex   = localSite['site_default_provider_index']

   centralSite                 = fullSiteList[NorNet_SiteIndex_Central]
   centralSiteProviders        = getNorNetProvidersForSite(centralSite)
   centralDefaultProviderIndex = centralSite['site_default_provider_index']

   return getTunnel(localSite, localSiteProviders[localDefaultProviderIndex],
                    centralSite, centralSiteProviders[centralDefaultProviderIndex],
                    version)



# ###########################################################################
# #### DNS Configuration                                                 ####
# ###########################################################################


# ###### Convert coordinate to degree/minutes/seconds #######################
def coordinateToDMS(coordinate, hemisphereTypes = 'NS'):
   hemisphere = hemisphereTypes[0]
   degrees = int(coordinate)
   if degrees < 0:
      degrees = abs(degrees)
      hemisphere = hemisphereTypes[1]

   m       = (coordinate % 1) * 60
   minutes = int(m)
   seconds = round((m % 1) * 60, 3)

   return(str(degrees) + ' ' + str(minutes) + ' ' + str(seconds) + ' ' + hemisphere)


# ###### Obtain location string #############################################
def getLocString(latitude, longitude, altitude, size, precision):
   return(coordinateToDMS(latitude) + ' ' +
          coordinateToDMS(longitude,'EW') + ' ' +
          str(altitude) + 'm ' +
          str(size) + 'm ' +
          str(precision) + 'm ' + str(precision) + 'm ')


# ###### Obtain RR type for address #########################################
def rrTypeForAddress(address):
   if address.version == 4:
      return('A')
   elif address.version == 6:
      return('AAAA')
   error('Bad address type!')


# ###### Add provider's name to hostname ####################################
def addProviderToName(hostName, providerName, domainName):
   hostName1 = getHostnameFromFQDN(hostName)
   if getDomainFromFQDN(hostName) != domainName:
      hostName2 = getHostnameFromFQDN(getDomainFromFQDN(hostName))
      if getDomainFromFQDN(getDomainFromFQDN(hostName)) != domainName:
         raise Exception('Bad host name/domain name: ' + hostName + '/' + domainName)
      hostName = hostName1 + '.' + hostName2
   else:
      hostName = hostName1

   newHostName = hostName + '.' + providerName + '.' + domainName
   # print hostName,domainName,newHostName
   return(newHostName)


# ###### Compute a serial number ############################################
def getSerial():
   value = datetime.datetime.utcnow() - datetime.datetime(2014,7,15)
   serial = value.days * (24 * 60 * 60) + value.seconds
   return serial


# ###### Write SOA ##########################################################
def writeSOA(outputFile, nsHostNameFQDN, siteFQDN, refreshTime, retryTime, expireTime, minTTL, defaultTTL, slaves):
   outputFile.write('$TTL ' + str(defaultTTL) + '\n\n')
   outputFile.write('@\tIN\tSOA\t' + nsHostNameFQDN + ' root.' + siteFQDN+ ' (\n')
   outputFile.write('\t{0:9d}   ; Serial\n'.format(getSerial()))
   outputFile.write('\t{0:9d}   ; Refresh time: interval for secondary DNS to check for updates (default: 3600)\n'.format(refreshTime))
   outputFile.write('\t{0:9d}   ; Retry time: time for retrying failed zone transfer (default: 600)\n'.format(retryTime))
   outputFile.write('\t{0:9d}   ; Expire time: when to expire a zone in case of failed zone transfer (default: 84600)\n'.format(expireTime))
   outputFile.write('\t{0:9d} ) ; Minimum TTL: minimum time-to-live (default: 3600)\n\n'.format(minTTL))
   outputFile.write('@\tIN\tNS\t' + nsHostNameFQDN + '\n')
   for slave in slaves:
      outputFile.write('@\tIN\tNS\t' + slave + '\n')
   outputFile.write('\n')


# ###### Write RR ###########################################################
def writeRR(outputFile, hostNameFQDN, record, value):
   outputFile.write('{0:42s}'.format(hostNameFQDN) + '\tIN\t' +
                    record + '\t' + value + '\n')


# ###### Write LOC RR #######################################################
def writeLOC(siteZoneFile, name, localSite):
   writeRR(siteZoneFile,
           name,
           'LOC',
           getLocString(localSite['site_latitude'],
                        localSite['site_longitude'],
                        localSite['site_altitude'],
                        5, 25))


# ###### Make SHA-1 fingerprint #############################################
def makeFingerprintSHA1(string):
   fp = hashlib.sha1()
   fp.update(string)
   return fp.hexdigest()


# ###### Make SHA-256 fingerprint ###########################################
def makeFingerprintSHA256(string):
   fp = hashlib.sha256()
   fp.update(string)
   return fp.hexdigest()


# ###### Write SSHFP RR #####################################################
# According to RFC 4255, RFC 6594 and RFC 7479
def writeSSHFP(siteZoneFile, name, publicKeys):
   if publicKeys != None:
      allPublicKeys = []
      if not isinstance(publicKeys, list):
         allPublicKeys = [ publicKeys ]
      else:
         for publicKey in publicKeys:
            allPublicKeys.append(unquote(publicKey))

      for publicKey in allPublicKeys:
         publicKeyType = -1
         # publicKey is like "ssh-rsa AAAA....."
         keyParameters = re.search(r'''^([^ ]+)\s+([^ ]+)''', publicKey)
         if keyParameters != None:
            if keyParameters.group(1) == 'ssh-rsa':
               publicKeyType = 1
            elif keyParameters.group(1) == 'ssh-dss':
               publicKeyType = 2
            elif re.match('^ecdsa-', keyParameters.group(1)):
               publicKeyType = 3
            elif keyParameters.group(1) == 'ssh-ed25519':
               publicKeyType = 4
            else:
               log('WARNING: Bad public key type "' + keyParameters.group(1) + '" -> skipping!')

         if publicKeyType > 0:
            decodedPublicKey = base64.b64decode(bytes(keyParameters.group(2), 'utf-8'))
            # print(name, makeFingerprintSHA1(decodedPublicKey))
            # print(name, makeFingerprintSHA256(decodedPublicKey))
            writeRR(siteZoneFile, name, 'SSHFP',
                    str(publicKeyType) + ' 1 ( ' + makeFingerprintSHA1(decodedPublicKey) + ' )')
            writeRR(siteZoneFile, name, 'SSHFP',
                    str(publicKeyType) + ' 2 ( ' + makeFingerprintSHA256(decodedPublicKey) + ' )')


# ###### Write zone configuration ###########################################
def writeZone(outputFile, zone, zoneFileName, masterSite):
   outputFile.write('zone "' + zone + '" IN {\n')
   if masterSite == None:
      outputFile.write('\ttype master;\n')
      outputFile.write('\tfile "/etc/bind/' + zoneFileName + '";\n')
      outputFile.write('\tallow-update { none; };\n')
      outputFile.write('\tallow-transfer { ')
      for version in [ 4, 6 ]:
         outputFile.write(str(makeNorNetIP(0, 0, 0, version)) + '; ')
         outputFile.write(str(makeNorNetTunnelIP(0, 0, 0, 0, version)) + '; ')
      outputFile.write('};\n')
   else:
      outputFile.write('\ttype slave;\n')
      outputFile.write('\tfile "/var/cache/bind/slaves/' + zoneFileName + '";\n')
      outputFile.write('\tmasters { ')
      for version in [ 6, 4 ]:
         outputFile.write(str(makeNorNetIP(masterSite['site_default_provider_index'],
                                           masterSite['site_index'],
                                           NorNet_NodeIndex_Tunnelbox,
                                           version).ip) + '; ')
      outputFile.write('};\n')
   outputFile.write('};\n\n')


# ###### Get NorNet node object for additional DNS entry ####################
def makeNodeForDNS(nodeName, site, nodeIndex, model, type, options):
   dnsName = makeNameFromUnicode(nodeName)
   v4Only = 0
   v6Only = 0
   for option in options:
      if option == 'v4only':
         v4Only = 1
      elif option == 'def-v4only':
         v4Only = 2
      elif option == 'v6only':
         v6Only = 1
      elif option == 'def-v6only':
         v6Only = 2
      elif option == 'slice':
         pass
      else:
         print('NOTE: Ignoring unknown option: ' + option)
   norNetNode = {
      'node_site_id'          : site['site_id'],
      'node_index'            : nodeIndex,
      'node_name'             : str.lower(dnsName['ascii'])     + '.' + site['site_domain'],
      'node_utf8'             : str.lower(dnsName['utf8'])  + '.' + site['site_domain'],
      'node_nornet_interface' : None,
      'node_model'            : model,
      'node_type'             : type,
      'node_state'            : 'MANUAL',
      'node_v4only'           : v4Only,
      'node_v6only'           : v6Only,
      'node_ssh_rsa_key'      : None,
      'node_tags'             : []
   }
   return norNetNode


# ###### Get list of slave servers ##########################################
def getSlavesForSite(fullSiteList, localSite):
   slaves = [ ]
   for remoteSiteIndex in fullSiteList:
      remoteSite = fullSiteList[remoteSiteIndex]
      if ( ((localSite['site_index'] == NorNet_SiteIndex_Central) and
            (remoteSite['site_index'] != localSite['site_index'])) or
           ((localSite['site_index'] != NorNet_SiteIndex_Central) and
            (remoteSite['site_index'] == NorNet_SiteIndex_Central)) ):
         # The remote site should be enabled as well!
         if remoteSite['site_enabled'] == True:
           slaves.append('ns.' + remoteSite['site_domain'] + '.')

   return slaves


# ###### Check whether to include node for given provider and IP version ####
def includeNodeIP(node, localSite, providerIndex, version):
   if ((node['node_v6only'] > 0) and (version == 4)):
      return False
   elif ((node['node_v6only'] > 1) and (providerIndex != localSite['site_default_provider_index'])):
      return False
   elif ((node['node_v4only'] > 0) and (version == 6)):
      return False
   elif ((node['node_v4only'] > 1) and (providerIndex != localSite['site_default_provider_index'])):
      return False
   return True


# ###### Generate hosts configuration #######################################
def makeBindConfiguration(fullSiteList, fullNodeList, fullSliceList,
                          localSite, hostName, additionalNodes, aliases):
   localSiteIndex    = localSite['site_index']
   localProviderList = getNorNetProvidersForSite(localSite)
   siteFQDN          = localSite['site_domain'] + '.'
   defaultTTL        = 24*3600
   minTTL            =    3600
   refreshTime       =     900
   retryTime         =      60
   expireTime        =   84600

   # Already-used node names and aliases
   usedNodes = { 'tunnelbox'     : 1,
                 'tunnelbox-ext' : 1,
                 'control-ext'   : 1,
                 'monitor'       : 1,
                 'nfs'           : 1,
                 'ns'            : 1,
                 'ntp'           : 1,
                 'plc'           : 1,
                 'proxy'         : 1,
                 'tftp'          : 1 }   # Reserved names

   # ====== Get hostname in default provider's network ======================
   localDefaultProvider       = localProviderList[localSite['site_default_provider_index']]
   hostNameForDefaultProvider = addProviderToName(hostName + '.' + siteFQDN, str.lower(localDefaultProvider['provider_short_name']), siteFQDN)

   siteZoneFile = codecs.open(siteFQDN + 'db', 'w', 'utf-8')
   writeAutoConfigInformation(siteZoneFile, ';')
   slaveServers = getSlavesForSite(fullSiteList, localSite)
   writeSOA(siteZoneFile, hostNameForDefaultProvider, siteFQDN, refreshTime, retryTime, expireTime, minTTL, defaultTTL, slaveServers)

   # ====== Some useful aliases =============================================
   if hostName != 'tunnelbox':
      writeRR(siteZoneFile, 'tunnelbox.' + siteFQDN, 'CNAME', hostNameForDefaultProvider)
   if hostName != 'tunnelbox-ext':
      writeRR(siteZoneFile, 'tunnelbox-ext.' + siteFQDN, 'CNAME',
              getHostnameFromFQDN(hostNameForDefaultProvider) + '-ext.' +
              getDomainFromFQDN(hostNameForDefaultProvider))
   if hostName != 'ns':
      writeRR(siteZoneFile, 'ns.' + siteFQDN, 'CNAME', hostNameForDefaultProvider)
   if hostName != 'ntp':
      writeRR(siteZoneFile, 'ntp.' + siteFQDN, 'CNAME', hostNameForDefaultProvider)
   if hostName != 'proxy':
      writeRR(siteZoneFile, 'proxy.' + siteFQDN, 'CNAME', hostNameForDefaultProvider)

   # ====== Prepare the list of hosts =======================================
   fullNodeList = fullNodeList + additionalNodes
   # Sort by node index
   fullNodeList.sort(key = lambda x: x['node_index'])

   # ====== Write forward lookup configuration ==============================
   for node in sorted(fullNodeList, key = lambda x: x['node_index']):
      # ====== Ensure that name is unique ===================================
      alreadyExisting = False
      try:
         if usedNodes[node['node_name']] != 0:
            alreadyExisting = True
      except:
         pass
      if alreadyExisting == True:
         error('Node name ' + node['node_name'] + ' is already in use (or a reserved name)!')
      usedNodes[node['node_name']] = 1

      siteZoneFile.write('\n; ====== ' + node['node_name'] + ' ======\n')
      for phase in [ 1, 2, 3 ]:
         for localProviderIndex in localProviderList:
            localProvider = localProviderList[localProviderIndex]

            # ====== Add internal addresses =================================
            for version in [ 4, 6 ]:
               # ====== Obtain the address ==================================
               try:
                  # Try to read the pseudo entry with the slice node index.
                  sliceNodeIndex = node['pseudo_slice_node_index']
               except:
                  # No slice, just a regular node.
                  sliceNodeIndex = 0

               nodeAddress = makeNorNetIP(localProviderIndex, localSiteIndex, node['node_index'], version, sliceNodeIndex)

               # ====== Hostname for default provider's address =============
               if phase == 1:
                  if localProviderIndex == localSite['site_default_provider_index']:
                     if version == 6:   # Just add entry once!
                        writeRR(siteZoneFile, node['node_name'] + '.', 'CNAME',
                                addProviderToName(node['node_name'] + '.',
                                                  str.lower(localProvider['provider_short_name']), siteFQDN))
                        # ====== Special aliases () ============================
                        # For PLC, Monitor, File Server
                        if ((localSite['site_index'] == NorNet_SiteIndex_PLC) and
                            (node['node_index'] == NorNet_NodeIndex_PLC) and
                            (node['node_name'] != 'plc')):
                           writeRR(siteZoneFile, 'plc.' + siteFQDN, 'CNAME',
                                   addProviderToName(node['node_name'] + '.',
                                                     str.lower(localProvider['provider_short_name']), siteFQDN))
                        if ((localSite['site_index'] == NorNet_SiteIndex_Monitor) and
                            (node['node_index'] == NorNet_NodeIndex_Monitor) and
                            (node['node_name'] != 'monitor')):
                           writeRR(siteZoneFile, 'monitor.' + siteFQDN, 'CNAME',
                                   addProviderToName(node['node_name'] + '.',
                                                     str.lower(localProvider['provider_short_name']), siteFQDN))
                           writeRR(siteZoneFile, siteFQDN, 'MX',
                                   '10 ' + addProviderToName(node['node_name'] + '.',
                                                             str.lower(localProvider['provider_short_name']), siteFQDN))
                        if ((localSite['site_index'] == NorNet_SiteIndex_FileSrv) and
                            (node['node_index'] == NorNet_NodeIndex_FileSrv)):
                           if (node['node_name'] != 'nfs'):
                              writeRR(siteZoneFile, 'nfs.' + siteFQDN, 'CNAME',
                                      addProviderToName(node['node_name'] + '.',
                                                        str.lower(localProvider['provider_short_name']), siteFQDN))
                           if (node['node_name'] != 'tftp'):
                               writeRR(siteZoneFile, 'tftp.' + siteFQDN, 'CNAME',
                                       addProviderToName(node['node_name'] + '.',
                                                         str.lower(localProvider['provider_short_name']), siteFQDN))

               # ====== Hostname for current provider's address =============
               elif phase == 2:
                  nameToWrite = addProviderToName(node['node_name'] + '.', str.lower(localProvider['provider_short_name']), siteFQDN)
                  if includeNodeIP(node, localSite, localProviderIndex, version):
                     writeRR(siteZoneFile, nameToWrite,
                             rrTypeForAddress(nodeAddress), str(nodeAddress.ip))
                  if version == 6:   # Just add entry once!
                     if (includeNodeIP(node, localSite, localProviderIndex, 4) or
                         includeNodeIP(node, localSite, localProviderIndex, 6)):
                        writeRR(siteZoneFile, nameToWrite,
                                'HINFO', '"' + node['node_model'] + '/' + localProvider['provider_long_name'] + '" "' + node['node_type'] + '"')
                        writeLOC(siteZoneFile, nameToWrite, localSite)
                        writeSSHFP(siteZoneFile, nameToWrite, node['node_ssh_rsa_key'])

               # ====== Hostname with *all* addresses =======================
               else:
                  if includeNodeIP(node, localSite, localProviderIndex, version):
                     writeRR(siteZoneFile,
                             addProviderToName(node['node_name'] + '.', 'all', siteFQDN),
                             rrTypeForAddress(nodeAddress),
                             str(nodeAddress.ip))
                  if ((localProviderIndex == localSite['site_default_provider_index']) and
                      (version == 6)):   # Just add entry once!
                     writeRR(siteZoneFile,
                             addProviderToName(node['node_name'] + '.', 'all', siteFQDN),
                             'HINFO', '"' + node['node_model'] + '" "' + node['node_type'] + '"')
                     writeLOC(siteZoneFile,
                              addProviderToName(node['node_name'] + '.', 'all', siteFQDN),
                              localSite)
                     writeSSHFP(siteZoneFile,
                                addProviderToName(node['node_name'] + '.', 'all', siteFQDN),
                                node['node_ssh_rsa_key'])

      # ====== Add aliases =========================================
      aliasList = []
      try:
         aliasList = aliases[node['node_index']]
      except:
         pass
      for i in range(0, len(aliasList)):
         aliasName = unquote(aliasList[i].strip()) + '.' + str.lower(localSite['site_domain'])
         try:
            if usedNodes[aliasName] != 0:
               log('WARNING: Skipping already-used (or reserved) alias "' + aliasName + '" for node ' + node['node_name'])
            aliasList[i] = str(node['node_name'])   # To be removed when sorting
         except:
            aliasList[i] = aliasName

      # Add punycode for IDN name
      if str(node['node_name']) != node['node_utf8']:
         aliasList.append(node['node_utf8'])

      # Remove duplicates
      aliasList = sorted(set(aliasList))

      # Write CNAMEs
      for aliasName in aliasList:
         usedNodes[aliasName] = 1
         punycodeName = str(aliasName.encode('idna').decode('utf-8'))
         if punycodeName != node['node_name']:
            for localProviderIndex in localProviderList:
               localProvider = localProviderList[localProviderIndex]

               if localProviderIndex == localSite['site_default_provider_index']:
                  writeRR(siteZoneFile,
                          punycodeName + '.',
                          'CNAME',
                          addProviderToName(node['node_name'] + '.', str.lower(localProvider['provider_short_name']), siteFQDN))

               if (includeNodeIP(node, localSite, localProviderIndex, 4) or
                   includeNodeIP(node, localSite, localProviderIndex, 6)):
                  writeRR(siteZoneFile,
                          addProviderToName(punycodeName + '.', str.lower(localProvider['provider_short_name']), siteFQDN),
                          'CNAME',
                          addProviderToName(node['node_name'] + '.', str.lower(localProvider['provider_short_name']), siteFQDN))

            writeRR(siteZoneFile,
                    addProviderToName(punycodeName + '.', 'all', siteFQDN),
                    'CNAME',
                    addProviderToName(node['node_name'] + '.', 'all', siteFQDN))
         #else:
            #print 'skip:', punycodeName

   # ====== Add external addresses of tunnelbox =============================
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]
      writeRR(siteZoneFile,
              hostName + '-ext.' + siteFQDN,
              'A', str(localProvider['provider_tunnelbox_ipv4'].ip))
      writeRR(siteZoneFile,
              addProviderToName(hostName + '-ext.' + siteFQDN, str.lower(localProvider['provider_short_name']), siteFQDN),
              'A', str(localProvider['provider_tunnelbox_ipv4'].ip))
      if localProvider['provider_tunnelbox_ipv6'].ip != IPv6Address('::'):
         writeRR(siteZoneFile,
                 hostName + '-ext.' + siteFQDN,
                 'AAAA', str(localProvider['provider_tunnelbox_ipv6'].ip))
         writeRR(siteZoneFile,
                 addProviderToName(hostName + '-ext.' + siteFQDN, str.lower(localProvider['provider_short_name']), siteFQDN),
                 'AAAA', str(localProvider['provider_tunnelbox_ipv6'].ip))

   # ====== Control box =====================================================
   # print localSite

   # ====== Tunnels =========================================================
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]
      siteZoneFile.write('\n; ====== ' + localProvider['provider_long_name'] + ' Tunnels ======\n')

      for remoteSiteIndex in fullSiteList:
         if remoteSiteIndex != localSiteIndex:
            remoteSite = fullSiteList[remoteSiteIndex]
            remoteProviderList = getNorNetProvidersForSite(remoteSite)
            for remoteProviderIndex in remoteProviderList:
               remoteProvider = remoteProviderList[remoteProviderIndex]
               for version in [ 4, 6 ]:
                  localTunnelIP  = makeNorNetTunnelIP(localSiteIndex, localProviderIndex,
                                                      remoteSiteIndex, remoteProviderIndex,
                                                      version)
                  remoteTunnelIP = makeNorNetTunnelIP(remoteSiteIndex, remoteProviderIndex,
                                                      localSiteIndex, localProviderIndex,
                                                      version)

                  localName  = str.lower(remoteProvider['provider_short_name']) + '.' + getHostnameFromFQDN(str.lower(remoteSite['site_domain'])) + '.' + \
                                      str.lower(localProvider['provider_short_name'])  + '.' + str.lower(localSite['site_domain'])  + '.'
                  writeRR(siteZoneFile, localName,
                          rrTypeForAddress(localTunnelIP), str(localTunnelIP.ip))

               writeRR(siteZoneFile, localName,
                       'HINFO', '"Tunnel Endpoint at ' + localSite['site_long_name'] + '/' + localProvider['provider_long_name'] + '" ' +
                       '"Remote Endpoint at ' + remoteSite['site_long_name'] + '/' + remoteProvider['provider_long_name'] + '"')
               writeLOC(siteZoneFile, localName, localSite)
               siteZoneFile.write('\n')

   siteZoneFile.close()


   # ====== Write reverse lookup configuration ==============================
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]
      for version in [ 4, 6 ]:
         providerNetwork  = makeNorNetIP(localProviderIndex, localSiteIndex, 0, version)
         providerZone     = getZoneForAddress(providerNetwork.network.network_address, providerNetwork.network.prefixlen)
         providerZoneFile = codecs.open(str.lower(localProvider['provider_short_name']) + '.' + siteFQDN + 'ipv' + str(version) + '.db', 'w', 'utf-8')
         writeSOA(providerZoneFile, hostNameForDefaultProvider, siteFQDN, refreshTime, retryTime, expireTime, minTTL, defaultTTL, slaveServers)

         for node in fullNodeList:
            if not includeNodeIP(node, localSite, localProviderIndex, version):
               continue

            # ====== Obtain the address ==================================
            try:
               # Try to read the pseudo entry with the slice node index.
               sliceNodeIndex = node['pseudo_slice_node_index']
            except:
               # No slice, just a regular node.
               sliceNodeIndex = 0

            providerAddress = makeNorNetIP(localProviderIndex, localSiteIndex, node['node_index'], version, sliceNodeIndex)
            prefixLength    = 32
            if version == 6:
               prefixLength = 128
            writeRR(providerZoneFile,
                    getZoneForAddress(providerAddress, prefixLength),
                    'PTR', addProviderToName(node['node_name'] + '.', str.lower(localProvider['provider_short_name']), siteFQDN))

         providerZoneFile.close()

   # ====== Write reverse lookup configuration for tunnels ==================
   if localSiteIndex == NorNet_SiteIndex_Central:
      for version in [ 4, 6 ]:
         tunnelNetwork  = makeNorNetTunnelIP(0, 0, 0, 0, version)
         tunnelZone     = getZoneForAddress(tunnelNetwork, tunnelNetwork.network.prefixlen)
         tunnelZoneFile = codecs.open('tunnels.ipv' + str(version) + '.db', 'w', 'utf-8')
         writeSOA(tunnelZoneFile, hostNameForDefaultProvider, siteFQDN, refreshTime, retryTime, expireTime, minTTL, defaultTTL, slaveServers)
         prefixLength = 32
         if version == 6:
            prefixLength = 128

         for outgoingSiteIndex in fullSiteList:
            outgoingSite = fullSiteList[outgoingSiteIndex]
            outgoingProviderList = getNorNetProvidersForSite(outgoingSite)

            for outgoingProviderIndex in outgoingProviderList:
               outgoingProvider = outgoingProviderList[outgoingProviderIndex]
               tunnelZoneFile.write('\n; ====== ' + outgoingProvider['provider_long_name'] + ' Tunnels for ' + outgoingSite['site_long_name'] + ' ======\n')

               for incomingSiteIndex in fullSiteList:
                  if incomingSiteIndex != outgoingSiteIndex:
                     incomingSite = fullSiteList[incomingSiteIndex]
                     incomingProviderList = getNorNetProvidersForSite(incomingSite)
                     for incomingProviderIndex in incomingProviderList:
                        incomingProvider = incomingProviderList[incomingProviderIndex]

                        outgoingTunnelIP = makeNorNetTunnelIP(outgoingSiteIndex, outgoingProviderIndex,
                                                              incomingSiteIndex, incomingProviderIndex,
                                                              version)
                        incomingTunnelIP = makeNorNetTunnelIP(incomingSiteIndex, incomingProviderIndex,
                                                              outgoingSiteIndex, outgoingProviderIndex,
                                                              version)
                        writeRR(tunnelZoneFile,
                                getZoneForAddress(outgoingTunnelIP.ip, prefixLength),
                                'PTR',
                                str.lower(incomingProvider['provider_short_name']) + '.' + getHostnameFromFQDN(str.lower(incomingSite['site_domain'])) + '.' +
                                str.lower(outgoingProvider['provider_short_name']) + '.' + str.lower(outgoingSite['site_domain']) + '.')

         tunnelZoneFile.close()


   # ====== Write zone configuration ========================================
   zoneConfFile = codecs.open('zones.conf', 'w', 'utf-8')
   writeAutoConfigInformation(zoneConfFile, '//')
   for siteIndex in fullSiteList:
      site       = fullSiteList[siteIndex]
      masterSite = site
      if siteIndex == localSiteIndex:
         masterSite = None

      zoneConfFile.write('// ====== ' + site['site_long_name'] + ' ======\n')
      if (masterSite == None) or (masterSite['site_enabled'] == True):
         writeZone(zoneConfFile, site['site_domain'], site['site_domain'] + '.db', masterSite)

         siteProviderList = getNorNetProvidersForSite(site)
         for siteProviderIndex in siteProviderList:
            siteProvider = siteProviderList[siteProviderIndex]
            for version in [ 4, 6 ]:
               providerNetwork = makeNorNetIP(siteProviderIndex, siteIndex, 0, version)
               providerZone = getZoneForAddress(providerNetwork, providerNetwork.network.prefixlen)
               writeZone(zoneConfFile,
                        providerZone,
                        str.lower(siteProvider['provider_short_name']) + '.' + site['site_domain'] + '.ipv' + str(version) + '.db',
                        masterSite)
      else:
         zoneConfFile.write('// Site is disabled!\n\n')

   zoneConfFile.write('\n// ====== Tunnel IP Reverse Lookup ======\n')
   # The Central Site tunnelbox is master for the reverse lookup of the tunnel IPs.
   if localSiteIndex == NorNet_SiteIndex_Central:
      masterSite = None
   else:
      masterSite = fullSiteList[NorNet_SiteIndex_Central]
   for version in [ 4, 6 ]:
      tunnelNetwork = makeNorNetTunnelIP(0, 0, 0, 0, version)
      tunnelZone    = getZoneForAddress(tunnelNetwork, tunnelNetwork.network.prefixlen)
      writeZone(zoneConfFile, tunnelZone, 'tunnels.ipv' + str(version) + '.db',
                masterSite)

   zoneConfFile.close()


   # ====== Write options file ==============================================
   optionsConfFile = codecs.open('named.conf.options', 'w', 'utf-8')
   writeAutoConfigInformation(optionsConfFile, '//')

   optionsConfFile.write('acl "nornet" {\n')
   for version in [ 4, 6 ]:
      optionsConfFile.write('\t' + str(makeNorNetIP(0, 0, 0, version)) + ';\n')
      optionsConfFile.write('\t' + str(makeNorNetTunnelIP(0, 0, 0, 0, version)) + ';\n')
   optionsConfFile.write('\tlocalhost;\n')
   optionsConfFile.write('\tlocalnets;\n')
   optionsConfFile.write('};\n\n')

   optionsConfFile.write("options {\n")
   optionsConfFile.write("\tdirectory          \"/var/cache/bind\";\n")
   optionsConfFile.write("// \tdnssec-enable      yes;\n")
   if getLocalSiteDNSValidateDNSSEC() == 'on':
      optionsConfFile.write("\tdnssec-validation  yes;\n")
   else:
      optionsConfFile.write("\tdnssec-validation  no;   // Setting for China!\n")
   optionsConfFile.write("// \tdnssec-lookaside   auto;\n")
   optionsConfFile.write("\tlisten-on          { any; };\n")
   optionsConfFile.write("\tlisten-on-v6       { any; };\n")
   optionsConfFile.write('\tallow-recursion    { nornet; };\n')
   optionsConfFile.write('\tallow-query        { nornet; };\n')
   optionsConfFile.write('\tallow-query-cache  { nornet; };\n')

   dnsForwardersString = getLocalSiteDNSForwardersString()
   if ((dnsForwardersString != None) and (dnsForwardersString != '')):
      dnsForwarders = shlex.split(getLocalSiteDNSForwardersString())
      if len(dnsForwarders) > 0:
         optionsConfFile.write("\tforwarders         {")
         for i in range(0, len(dnsForwarders)):
            try:
               address = unquote(dnsForwarders[i])
               dnsForwarder = ip_address(address)
               optionsConfFile.write(' ' + str(dnsForwarder) + ';')

            except Exception as e:
               error('Bad configuration "' + dnsForwarders[i] + '" for NorNet_LocalSite_DNS_Forwarders: ' + str(e))
         optionsConfFile.write(" };\n")
         optionsConfFile.write("\tforward            only;\n")

   optionsConfFile.write("};\n")

   optionsConfFile.close()



# ###########################################################################
# #### DHCP Configuration                                                ####
# ###########################################################################


# ###### Generate DHCP daemon configuration #################################
def makeDHCPDConfiguration(localSite, dynamicStart, dynamicEnd, staticList, denyList):
   domain        = localSite['site_domain']
   providerIndex = localSite['site_default_provider_index']
   siteIndex     = localSite['site_index']
   siteNetwork   = makeNorNetIP(providerIndex, siteIndex, 0, 4)
   siteTunnelbox = makeNorNetIP(providerIndex, siteIndex, NorNet_NodeIndex_Tunnelbox, 4)

   outputFile = codecs.open('dhcpd-config', 'w', 'utf-8')
   writeAutoConfigInformation(outputFile)

   outputFile.write('ddns-updates               off;\n')
   outputFile.write('option domain-name         "' + domain + '";\n')
   outputFile.write('option domain-name-servers ' + str(siteTunnelbox.ip) + ';\n')
   outputFile.write('default-lease-time         3600;\n')
   outputFile.write('max-lease-time             86400;\n')
   outputFile.write('allow                      booting;\n')
   outputFile.write('allow                      bootp;\n\n')
   outputFile.write('authoritative;\n\n')

   outputFile.write('subnet ' + str(siteNetwork.ip) + ' netmask ' + str(siteNetwork.netmask) + ' { \n')
   if ((dynamicStart > 1) and (dynamicEnd <= NorNet_MaxNodeIndex) and (dynamicEnd >= dynamicStart)):
      outputFile.write('\trange ' +
                     str(makeNorNetIP(providerIndex, siteIndex, dynamicStart, 4).ip) + ' ' +
                     str(makeNorNetIP(providerIndex, siteIndex, dynamicEnd, 4).ip) + ' ' +
                     ';\n')
   outputFile.write('\toption routers ' + str(siteTunnelbox.ip) + ';\n')
   outputFile.write('}\n')

   for staticEntry in staticList:
      outputFile.write('\nhost node' + str(staticEntry['node_index']) + ' {\n')
      outputFile.write('\thardware ethernet ' + staticEntry['node_mac'] + ';\n')
      outputFile.write('\tfixed-address     ' + str(makeNorNetIP(providerIndex, siteIndex, staticEntry['node_index'], 4).ip) + ';\n')
      if len(staticEntry['node_filename']) > 0:
         outputFile.write('\tfilename          "' + staticEntry['node_filename'] + '";\n')
         if staticEntry['node_servername'] != '':
            # outputFile.write('\tserver-name       "' + staticEntry['node_servername'] + '";\n')
            outputFile.write('\tnext-server       ' + staticEntry['node_servername'] + ';\n')
         else:
            # outputFile.write('\tserver-name       "tftp.' + getCentralSiteDomainName() + '";\n')
            outputFile.write('\tnext-server       tftp.' + getCentralSiteDomainName() + ';\n')
      outputFile.write('}\n')

   for denyEntry in denyList:
      label = 'deny-' + denyEntry.replace(':', '').upper()
      outputFile.write('\nhost ' + label + ' {\n')
      outputFile.write('\thardware ethernet ' + denyEntry + ';\n')
      outputFile.write('\tdeny booting;\n')
      outputFile.write('}\n')

   outputFile.close()



# ###########################################################################
# #### RADVD Configuration                                               ####
# ###########################################################################


# ###### Generate RADVD daemon configuration ################################
def makeRADVDConfiguration(localSite):
   domain        = localSite['site_domain']
   providerIndex = localSite['site_default_provider_index']
   siteIndex     = localSite['site_index']
   siteNetwork   = makeNorNetIP(providerIndex, siteIndex, 0, 4)
   siteTunnelbox = makeNorNetIP(providerIndex, siteIndex, NorNet_NodeIndex_Tunnelbox, 4)

   outputFile = codecs.open('radvd-config', 'w', 'utf-8')

   if getLocalSiteRADVDConfigurationString() == 'on':
      writeAutoConfigInformation(outputFile)
      outputFile.write('interface ' + getLocalNodeNorNetInterface() + ' {\n ')
      outputFile.write('\tAdvSendAdvert on;\n')

      providerList = getNorNetProvidersForSite(localSite)
      for onlyDefault in [ True, False ]:
         for providerIndex in providerList:
            if ( ((onlyDefault == True)  and (providerIndex == localSite['site_default_provider_index'])) or \
                 ((onlyDefault == False) and (providerIndex != localSite['site_default_provider_index'])) ):
               provider  = providerList[providerIndex]
               tunnelbox = makeNorNetIP(providerIndex, siteIndex, NorNet_NodeIndex_Tunnelbox, 6)

               outputFile.write('\n\t# ' + provider['provider_long_name'] + '\n')
               outputFile.write('\tprefix ' + str(tunnelbox) + ' {\n')
               outputFile.write('\t\tAdvOnLink on;\n')
               outputFile.write('\t\tAdvAutonomous on;\n')
               outputFile.write('\t\tAdvRouterAddr on;\n')
               outputFile.write('\t};\n')

      outputFile.write('};\n')

   outputFile.close()



# ###########################################################################
# #### Squid Configuration                                               ####
# ###########################################################################


# ###### Generate Squid daemon configuration ################################
def makeSquidConfiguration(nodeName, localSite):
   outputFile = codecs.open('squid-config', 'w', 'utf-8')

   # Try to obtain Squid version
   try:
      result = subprocess.Popen("/usr/bin/dpkg -s squid3 | grep 'Version' | awk '{ print $2 }'", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
      version = result.stdout.read().decode('utf-8')
   except:
      version = '999.999'

   outputFile.write('# ================== Proxy =============================\n')
   outputFile.write('visible_hostname ' + nodeName + '\n')
   outputFile.write('via on\n')
   outputFile.write('# debug_options ALL,2\n\n')

   outputFile.write('# ================== ACLs ==============================\n')
   if re.match('^(3\.0\.|3\.1\.)', version):
      outputFile.write('acl manager proto cache_object\n')

   outputFile.write('# ' + localSite['site_long_name'] + ' site:\n')
   providerList = getNorNetProvidersForSite(localSite)
   for providerIndex in providerList:
      for version in [ 4, 6 ]:
         localNetwork = makeNorNetIP(providerIndex, localSite['site_index'], 0, version)
         outputFile.write('acl localnet src ' + str(localNetwork) + '\n')

   # Non-Central site tunnelboxes may forward requests to Central Site tunnelbox:
   if localSite['site_index'] == NorNet_SiteIndex_Central:
      outputFile.write('\n# Remote tunnelboxes:\n')
      for version in [ 4, 6 ]:
         outputFile.write('acl localnet src ' + str(makeNorNetTunnelIP(0, 0, 0, 0, version)) + '\n')

   additionalNetworks = shlex.split(getSquidAdditionalNetworks())
   for i in range(0, len(additionalNetworks)):
      try:
         network = ip_interface(additionalNetworks[i])
         if i == 0:
            outputFile.write('\n# Additional networks:\n')
         outputFile.write('acl localnet src ' + str(network) + '\n')

      except Exception as e:
         error('Bad configuration "' + additionalNetworks[i] + '" for NorNet_Squid_AdditionalNetworks: ' + str(e))

   outputFile.write('\n# Ports:\n')
   outputFile.write('acl SSL_ports port 443\n')
   outputFile.write('acl Safe_ports port 80          # http\n')
   outputFile.write('acl Safe_ports port 21          # ftp\n')
   outputFile.write('acl Safe_ports port 443         # https\n')
   outputFile.write('acl Safe_ports port 70          # gopher\n')
   outputFile.write('acl Safe_ports port 210         # wais\n')
   outputFile.write('acl Safe_ports port 1025-65535  # unregistered ports\n')
   outputFile.write('acl Safe_ports port 280         # http-mgmt\n')
   outputFile.write('acl Safe_ports port 488         # gss-http\n')
   outputFile.write('acl Safe_ports port 591         # filemaker\n')
   outputFile.write('acl Safe_ports port 777         # multiling http\n')
   outputFile.write('acl CONNECT method CONNECT\n\n')

   outputFile.write('# ================== HTTP Access =======================\n')
   outputFile.write('http_access allow manager localhost\n')
   outputFile.write('http_access deny manager\n')
   outputFile.write('http_access deny !Safe_ports\n')
   outputFile.write('http_access deny CONNECT !SSL_ports\n')
   outputFile.write('http_access allow localhost\n')
   outputFile.write('http_access allow localnet\n')
   outputFile.write('http_access deny all\n')
   outputFile.write('http_port 3128\n')
   if getSquidTransparentProxy() == 'on':
      outputFile.write('http_port 3129 transparent\n')
   # outputFile.write('extension_methods REPORT MERGE MKACTIVITY CHECKOUT PROPFIND\n\n')

   outputFile.write('# Improve privacy:\n')
   # NOTE: Commented out!
   # - request_header_access causes problems with CDNs, resulting in HTTP 403.
   # - request_header_replace user agent change even makes user agents more unique.
   # outputFile.write('request_header_access User-Agent deny all\n')
   # outputFile.write('request_header_replace User-Agent Mozilla/5.0 (compatible)\n')
   outputFile.write('forwarded_for off\n\n')

   outputFile.write('# ================== Caching ===========================\n')
   for squidDirectory in [ '/var/spool/squid3', '/var/spool/squid' ]:
      if os.path.isdir(squidDirectory):
         break
   outputFile.write('logformat squid %{%Y-%m-%dT%H:%M:%S}tl.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %un %Sh/%<A %mt\n')
   outputFile.write('coredump_dir ' + squidDirectory + '\n')
   parentCache = getSquidParentCache()
   if parentCache != '':
      outputFile.write('cache_peer ' + parentCache + ' parent 3128 0 no-query default\n')
      outputFile.write('never_direct allow all\n')
      outputFile.write('nonhierarchical_direct off\n')
   diskCacheSize = getSquidDiskCacheSize()
   if diskCacheSize > 0:
      outputFile.write('cache_dir aufs ' + squidDirectory + ' ' + str(diskCacheSize) + ' 16 256\n')
   memoryCacheSize = getSquidMemoryCacheSize()
   if memoryCacheSize < 32:
      memoryCacheSize = 32
   outputFile.write('cache_mem ' + str(memoryCacheSize) + ' MB\n')
   maxObjectSize = getSquidMaxObjectSize()
   if maxObjectSize == 0:
      maxObjectSize = max(1, memoryCacheSize / 16)
   outputFile.write('maximum_object_size ' + str(maxObjectSize) + ' MB\n\n')

   outputFile.write('# ================== Refresh Patterns ==================\n')
   outputFile.write('refresh_pattern ^ftp:           1440    20%     10080\n')
   outputFile.write('refresh_pattern ^gopher:        1440    0%      1440\n')
   outputFile.write('refresh_pattern -i (/cgi-bin/|\?) 0     0%      0\n')
   outputFile.write('refresh_pattern (Release|Packages(.gz)*)$      0       20%     2880\n')
   outputFile.write('refresh_pattern .               0       20%     4320\n')

   outputFile.close()



# ###########################################################################
# #### IPSEC Configuration                                               ####
# ###########################################################################


# ###### Make a key from a secret ###########################################
def makeKey(string):
   key = hashlib.sha512()
   key.update(string)
   return key.hexdigest()


# ###### Write security association #########################################
def _writeSecAssoc(outputFile, localAddress, remoteAddress, inIndex, outIndex, ahKey, espKey):
   outputFile.write('add ' + str(localAddress)  + ' ' + str(remoteAddress) + ' ah  ' + str((inIndex * 65536)  + outIndex) + ' -A hmac-sha512 "' + ahKey + '";\n')
   outputFile.write('add ' + str(remoteAddress) + ' ' + str(localAddress)  + ' ah  ' + str((outIndex * 65536) + inIndex)  + ' -A hmac-sha512 "' + ahKey + '";\n')
   outputFile.write('add ' + str(localAddress)  + ' ' + str(remoteAddress) + ' esp ' + str((inIndex * 65536)  + outIndex) + ' -E camellia-cbc "' + espKey + '";\n')
   outputFile.write('add ' + str(remoteAddress) + ' ' + str(localAddress)  + ' esp ' + str((outIndex * 65536) + inIndex)  + ' -E camellia-cbc "' + espKey + '";\n')
   outputFile.write('spdadd ' + str(localAddress) + ' ' + str(remoteAddress) + ' any -P out ipsec\n')
   outputFile.write('   esp/transport//require\n')
   outputFile.write('   ah/transport//require;\n')
   outputFile.write('spdadd ' + str(remoteAddress) + ' ' + str(localAddress) + ' any -P in ipsec\n')
   outputFile.write('   esp/transport//require\n')
   outputFile.write('   ah/transport//require;\n\n')


# ###### Generate Squid daemon configuration ################################
def makeIPSECConfiguration(localSite):
   localSiteIndex       = localSite['site_index']
   localProviderList    = getNorNetProvidersForSite(localSite)
   localAddress         = makeNorNetIP(localSite['site_default_provider_index'], localSiteIndex, NorNet_NodeIndex_Tunnelbox, 6).ip
   centralSite          = fullSiteList[NorNet_SiteIndex_Central]
   centralSiteProviders = getNorNetProvidersForSite(centralSite)

   outputFile = codecs.open('ipsec-config', 'w', 'utf-8')
   outputFile.write('#!/sbin/setkey -f\n')
   writeAutoConfigInformation(outputFile)

   outputFile.write('flush;\n')
   outputFile.write('spdflush;\n\n')


   # ########################################################################
   # WARNING: This static configuration is not intended for high security!
   # It is just so block annying packet modification effects by firewalls.
   # ########################################################################
   staticAHKey  = 'e1f092a4855dfa3ebbf9be999c5c05a9157c596a8647082c95ff13737f6e4928'
   staticESPKey = '9217312ad708a4127b36d75219f4b087'


   if localSiteIndex == NorNet_SiteIndex_Central:
      for remoteSiteIndex in fullSiteList:
         if remoteSiteIndex != localSiteIndex:
            remoteSite = fullSiteList[remoteSiteIndex]
            remoteSiteProviders = getNorNetProvidersForSite(remoteSite)
            remoteAddress = makeNorNetIP(remoteSite['site_default_provider_index'], remoteSiteIndex, NorNet_NodeIndex_Tunnelbox, 6).ip
            ahKey  = makeKey(staticAHKey  + str(localAddress) + '-' + str(remoteAddress))[0:64]
            espKey = makeKey(staticESPKey + str(localAddress) + '-' + str(remoteAddress))[0:32]
            _writeSecAssoc(outputFile, localAddress, remoteAddress,
                           (localSite['site_default_provider_index']  * 256) + NorNet_SiteIndex_Central,
                           (remoteSite['site_default_provider_index'] * 256) + remoteSiteIndex,
                           ahKey, espKey)

   else:
      remoteAddress = makeNorNetIP(centralSite['site_default_provider_index'], localSiteIndex, NorNet_NodeIndex_Tunnelbox, 6).ip
      ahKey  = makeKey(staticAHKey  + str(remoteAddress) + '-' + str(localAddress))[0:64]
      espKey = makeKey(staticESPKey + str(remoteAddress) + '-' + str(localAddress))[0:32]
      _writeSecAssoc(outputFile, localAddress, remoteAddress,
                     (localSite['site_default_provider_index']   * 256) + localSiteIndex,
                     (centralSite['site_default_provider_index'] * 256) + NorNet_SiteIndex_Central,
                     ahKey, espKey)


# ###########################################################################
# #### RSerPool Configuration                                            ####
# ###########################################################################


# ###### Generate RSerPool Registrar configuration ##########################
def makeRegistrarConfiguration(localSite):
   localSiteIndex       = localSite['site_index']
   localProviderList    = getNorNetProvidersForSite(localSite)
   localAddresses       = [ IPv6Interface('::1/128'), IPv4Interface('127.0.0.1/32') ]
   centralSite          = fullSiteList[NorNet_SiteIndex_Central]
   centralSiteProviders = getNorNetProvidersForSite(centralSite)

   outputFile = codecs.open('registrar-config', 'w', 'utf-8')
   writeAutoConfigInformation(outputFile)

   # ====== ASAP and ENRP endpoitns =========================================
   options           = ''
   asapAnnounce      = 'off'
   enrpAnnounce      = 'off'
   asap              = ''
   enrp              = ''
   firstAddress      = True
   for onlyDefault in [ True, False ]:
      for providerIndex in localProviderList:
         if ( ((onlyDefault == True)  and (providerIndex == localSite['site_default_provider_index'])) or \
              ((onlyDefault == False) and (providerIndex != localSite['site_default_provider_index'])) ):
            for version in [ 6, 4 ]:
               localAddresses.append(makeNorNetIP(providerIndex, localSiteIndex, NorNet_NodeIndex_Tunnelbox, version))
   for address in localAddresses:
      if firstAddress:
         asap         = '[' + str(address.ip) + ']:3863'
         enrp         = '[' + str(address.ip) + ']:9901'
         firstAddress = False
      else:
         asap = asap + ',[' + str(address.ip) + ']'
         enrp = enrp + ',[' + str(address.ip) + ']'

   # ====== ENRP peers ======================================================
   if localSiteIndex != NorNet_SiteIndex_Central:
      options              = options + ' -peer='
      firstAddress         = True
      for onlyDefault in [ True, False ]:
         for providerIndex in localProviderList:
            for version in [ 6, 4 ]:
               address = makeNorNetIP(providerIndex,
                                      NorNet_SiteIndex_Central,
                                      NorNet_NodeIndex_Tunnelbox,
                                      version)
               if firstAddress:
                  options = options + '[' + str(address.ip) + ']:9901'
                  firstAddress = False
               else:
                  options = options + ',[' + str(address.ip) + ']'

   # ====== CSP monitor =====================================================
   cspMonitor = makeNorNetIP(centralSite['site_default_provider_index'],
                             NorNet_SiteIndex_Central,
                             NorNet_NodeIndex_Monitor, 6)

   # ====== Registrar options ===============================================
   outputFile.write('DAEMON_ARGS="$DAEMON_ARGS' + \
                    ' -asapannounce=' + asapAnnounce + \
                    ' -enrpannounce=' + enrpAnnounce + \
                    ' -asap=' + asap + \
                    ' -enrp=' + enrp + \
                    ' -cspserver=[' + str(cspMonitor.ip) + ']:2960' + \
                    ' -cspinterval=10000' + \
                    options + \
                    ' -logcolor=off"\n')

   outputFile.close()



# ###########################################################################
# #### Tunnel and Routing Configuration                                  ####
# ###########################################################################


# ###### Get ID for routing table ###########################################
def getTableID(level, opIndex):
   return (level * 1000000) + opIndex


# ###### Get preference for routing table ###################################
def getTablePref(opIndex, level):
   return 1000 + (1000 * level) + opIndex


# ###### Make IPv6 link-local address for GRE tunnel ########################
def makeGRELinkLocal(networkID,
                     localProviderIndex, localSiteIndex,
                     remoteProviderIndex, remoteSiteIndex):
   # Needs separate spaces for "gre" and "seks" interfaces!
   # This feature is provided by networkID.
   result = IPv6Interface('fe80::' + \
                        str.replace(hex(networkID),           '0x', '') + ':' + \
                        str.replace(hex(localProviderIndex),  '0x', '') + ':' + \
                        str.replace(hex(localSiteIndex),      '0x', '') + ':' + \
                        str.replace(hex(remoteProviderIndex), '0x', '') + ':' + \
                        str.replace(hex(remoteSiteIndex),     '0x', '') + \
                        '/64')
   return(result)


# ###### Add routes to remote tunnel endpoints ##############################
# From a site A, ensure that the endpoints of a tunnel B <-> C get reachable.
# In this example, A = local site, B = remote site, C = peer site.
def makeRemoteSiteTunnelRoutes(fullSiteList, localSite, version,
                               nextHopInterface, nextHopAddress,
                               remoteSite):
   additionalStaticRoutes = []
   outgoingProviderList   = getNorNetProvidersForSite(remoteSite)
   for outgoingProviderIndex in outgoingProviderList:
      outgoingProvider = outgoingProviderList[outgoingProviderIndex]
      for peerSiteIndex in fullSiteList:
         if peerSiteIndex == localSite['site_index']:    # Not to local site
            continue
         if peerSiteIndex == remoteSite['site_index']:   # Not the remote site
            continue
         peerSite         = fullSiteList[peerSiteIndex]
         peerProviderList = getNorNetProvidersForSite(peerSite)

         for incomingProviderIndex in peerProviderList:
            incomingProvider = peerProviderList[incomingProviderIndex]
            tunnel           = getTunnel(remoteSite, outgoingProvider, peerSite, incomingProvider, version)
            if tunnel['tunnel_local_inner_address'].ip < tunnel['tunnel_remote_inner_address'].ip:
               # Route over remote site, but only if the "lower" side of the tunnel is on this site!
               destNetwork = ip_interface(str(tunnel['tunnel_local_inner_address'].network.network_address) + '/' + \
                                       str(tunnel['tunnel_local_inner_address'].network.prefixlen))
               additionalStaticRoutes.append(destNetwork)

   return additionalStaticRoutes


# ###### Generate tunnelbox configuration ###################################
def makeTunnelboxConfiguration(fullSiteList, localSite):
   localSiteIndex    = localSite['site_index']
   localProviderList = getNorNetProvidersForSite(localSite)

   log('Making tunnelbox configuration for ' + localSite['site_long_name'] + ' ...')


   # ====== Generate tunnelbox configuration file ===========================
   tunnelboxFile = codecs.open('tunnelbox-config', 'w', 'utf-8')

   tunnelboxFile.write('TUNNELBOX_SITE_INDEX=' + str(localSite['site_index']) + '\n')

   tunnelboxFile.write('NORNET_INTERFACE="' + str(NorNet_Configuration['NorNet_LocalNode_NorNetInterface']) + '"\n')
   tunnelboxFile.write('NORNET_NETWORK_IPv4="' + str(NorNet_Configuration['NorNet_IPv4Prefix']) + '"\n')
   tunnelboxFile.write('NORNET_NETWORK_IPv6="' + str(NorNet_Configuration['NorNet_IPv6Prefix']) + '"\n')
   tunnelboxFile.write('NORNET_TUNNEL_NETWORK_IPv4="' + str(NorNet_Configuration['NorNet_IPv4TunnelPrefix']) + '"\n')
   tunnelboxFile.write('NORNET_TUNNEL_NETWORK_IPv6="' + str(NorNet_Configuration['NorNet_IPv6TunnelPrefix']) + '"\n')


   # ====== Generate per-provider configurations ============================
   pathNumber = 0
   providerSet = set([])

   for localProviderOnlyDefault in [ True, False ]:
      for localProviderIndex in localProviderList:
         if ( ((localProviderOnlyDefault == True)  and (localProviderIndex == localSite['site_default_provider_index'])) or \
              ((localProviderOnlyDefault == False) and (localProviderIndex != localSite['site_default_provider_index'])) ):

            localProvider = localProviderList[localProviderIndex]

            # ====== Settings for IP rules ==================================
            externalRoutingTableID         = getTableID(1, localProviderIndex)
            externalRoutingTablePref       = getTablePref(localProviderIndex, 0)

            internalRoutingTableID         = getTableID(2, localProviderIndex)
            internalRoutingTableDestPref   = getTablePref(localProviderIndex, 1)
            internalRoutingTableTOSPref    = getTablePref(localProviderIndex, 2)
            internalRoutingTableSourcePref = getTablePref(localProviderIndex, 3)

            if pathNumber + 1 < len(NorNet_TOSSettings):
               internalRoutingTableTOS = NorNet_TOSSettings[pathNumber + 1]
            else:
               internalRoutingTableTOS = 0x00


            # ====== Generate provider configuration file ===================
            providerFile = codecs.open(str.lower(localProvider['provider_short_name']) + '.provider', 'w', 'utf-8')
            providerFile.write('PROVIDER_INDEX='                      + str(localProvider['provider_index']) + '\n')
            providerFile.write('PROVIDER_INTERFACE="'                 + localProvider['provider_tunnelbox_interface'] + '"\n')
            providerFile.write('PROVIDER_MTU='                        + str(localProvider['provider_mtu']) + '\n')
            providerFile.write('PROVIDER_SHORT_NAME="'                + str.lower(localProvider['provider_short_name']) + '"\n')
            providerFile.write('PROVIDER_LONG_NAME="'                 + localProvider['provider_long_name']  + '"\n')
            providerFile.write('PROVIDER_TOS='                        + str(internalRoutingTableTOS)         + '\n')
            if localProviderIndex == localSite['site_default_provider_index']:
               providerFile.write('PROVIDER_IS_DEFAULT_PROVIDER=1\n\n')
            else:
               providerFile.write('PROVIDER_IS_DEFAULT_PROVIDER=0\n\n')

            providerFile.write('PROVIDER_EXTERNAL_TABLE='             + str(externalRoutingTableID)         + '\n')
            providerFile.write('PROVIDER_EXTERNAL_TABLE_PREF='        + str(externalRoutingTablePref)       + '\n')
            providerFile.write('PROVIDER_INTERNAL_TABLE='             + str(internalRoutingTableID)         + '\n')
            providerFile.write('PROVIDER_INTERNAL_TABLE_DEST_PREF='   + str(internalRoutingTableDestPref)   + '\n')
            providerFile.write('PROVIDER_INTERNAL_TABLE_TOS_PREF='    + str(internalRoutingTableTOSPref)    + '\n')
            providerFile.write('PROVIDER_INTERNAL_TABLE_SOURCE_PREF=' + str(internalRoutingTableSourcePref) + '\n')

            externalNetworkIPv4 = IPv4Interface(str(localProvider['provider_tunnelbox_ipv4'].network.network_address) + '/' + \
                                              str(localProvider['provider_tunnelbox_ipv4'].network.prefixlen))
            providerFile.write('PROVIDER_EXTERNAL_NETWORK_IPv4="' + str(externalNetworkIPv4) + '"\n')
            providerFile.write('PROVIDER_EXTERNAL_GATEWAY_IPv4="' + str(localProvider['provider_gateway_ipv4']) + '"\n')
            providerFile.write('PROVIDER_EXTERNAL_TUNNELBOX_IPv4="' + str(localProvider['provider_tunnelbox_ipv4'].ip) + '"\n')


            externalNetworkIPv6 = IPv6Interface(str(localProvider['provider_tunnelbox_ipv6'].network.network_address) + '/' + \
                                              str(localProvider['provider_tunnelbox_ipv6'].network.prefixlen))
            if externalNetworkIPv6.network.prefixlen > 0:
               providerFile.write('PROVIDER_EXTERNAL_NETWORK_IPv6="' + str(externalNetworkIPv6) + '"\n')
               providerFile.write('PROVIDER_EXTERNAL_GATEWAY_IPv6="' + str(localProvider['provider_gateway_ipv6']) + '"\n')
               providerFile.write('PROVIDER_EXTERNAL_TUNNELBOX_IPv6="' + str(localProvider['provider_tunnelbox_ipv6'].ip) + '"\n')

            providerList = getNorNetProvidersForSite(localSite)
            for version in [ 4, 6 ]:
               localProviderNetwork = makeNorNetIP(localProviderIndex, localSiteIndex, 0, version)
               providerFile.write('PROVIDER_INTERNAL_NETWORK_IPv' + str(version) + '="' + str(localProviderNetwork) + '"\n')
               tunnelboxAddress = makeNorNetIP(localProviderIndex, localSiteIndex, NorNet_NodeIndex_Tunnelbox, version)
               providerFile.write('PROVIDER_INTERNAL_TUNNELBOX_IPv' + str(version) + '="' + str(tunnelboxAddress) + '"\n')


            providerFile.close()

            tunnelboxFile.write('PROVIDER_FOR_INTERFACE_' + localProvider['provider_tunnelbox_interface'] + \
                                '="' + str.lower(localProvider['provider_short_name']) + '"\n')
            providerSet.add(str.lower(localProvider['provider_short_name']))


            # ====== Generate interface configuration files =================
            for remoteSiteIndex in fullSiteList:
               if remoteSiteIndex == localSiteIndex:
                  continue
               remoteSite         = fullSiteList[remoteSiteIndex]
               remoteProviderList = getNorNetProvidersForSite(remoteSite)

               for remoteProviderOnlyDefault in [ True, False ]:
                  for remoteProviderIndex in remoteProviderList:
                     if ( ((remoteProviderOnlyDefault == True)  and (remoteProviderIndex == remoteSite['site_default_provider_index'])) or \
                          ((remoteProviderOnlyDefault == False) and (remoteProviderIndex != remoteSite['site_default_provider_index'])) ):

                        remoteProvider = remoteProviderList[remoteProviderIndex]

                        for version in [ 4, 6 ]:
                           tunnel          = getTunnel(localSite, localProvider, remoteSite, remoteProvider, version)
                           remoteNetwork   = makeNorNetIP(remoteProviderIndex, remoteSiteIndex, 0, version)
                           remoteTunnelbox = makeNorNetIP(remoteProviderIndex, remoteSiteIndex, NorNet_NodeIndex_Tunnelbox, version).ip

                           interfaceFile = codecs.open(tunnel['tunnel_interface'] + '.interface' + str(version), 'w', 'utf-8')
                           interfaceFile.write('INTERFACE_DESCRIPTION="' + \
                              localProvider['provider_long_name'] + ' (' + str(localProvider['provider_index']) + ') -> ' + \
                              remoteSite['site_utf8'] + ' (' + str(remoteSite['site_index']) + ') / ' + \
                              remoteProvider['provider_long_name'] + ' (' + str(remoteProvider['provider_index']) + ')"\n')

                           # ====== Tunnel ==================================
                           needsLinkLocalAddress = True
                           tunnelOverhead        = None
                           if ((version == 4) and (re.match('^gre', tunnel['tunnel_interface']))):
                              interfaceFile.write('INTERFACE_TYPE="gre"\n')
                              interfaceFile.write('GRE_KEY="' + hex(tunnel['tunnel_key']) + '"\n')
                              tunnelOverhead = 20 + 8   # IPv4 + GRE[with key]
                           elif ((version == 6) and (tunnel['tunnel_over_ipv4'] == True)):
                              interfaceFile.write('INTERFACE_TYPE="existing-gre"\n')
                              needsLinkLocalAddress = False
                              tunnelOverhead = 20 + 8   # IPv4 + GRE[with key]
                           else:
                              interfaceFile.write('INTERFACE_TYPE="seks"\n')
                              tunnelOverhead = 48   # Based on 1452 bytes MTU on IPv6-over-IPv6 tunnels by default.

                           interfaceFile.write('INTERFACE_PROVIDER="' + str.lower(localProvider['provider_short_name']) + '"\n\n')

                           if needsLinkLocalAddress == True:
                              interfaceFile.write('TUNNEL_V6LINKLOCAL_ADDRESS="' + \
                                 str(makeGRELinkLocal(version * 0x1111,
                                                      localProviderIndex, localSiteIndex,
                                                      remoteProviderIndex, remoteSiteIndex)) + '"\n\n')

                           interfaceFile.write('TUNNEL_LOCAL_OUTER_ADDRESS="'  + str(tunnel['tunnel_local_outer_address'].ip)  + '"\n')
                           interfaceFile.write('TUNNEL_REMOTE_OUTER_ADDRESS="' + str(tunnel['tunnel_remote_outer_address'].ip) + '"\n')
                           interfaceFile.write('TUNNEL_OUTER_PREFIX='          + str(tunnel['tunnel_local_outer_address'].network.prefixlen) + '\n\n')

                           interfaceFile.write('TUNNEL_LOCAL_INNER_ADDRESS="'  + str(tunnel['tunnel_local_inner_address'].ip)  + '"\n')
                           interfaceFile.write('TUNNEL_REMOTE_INNER_ADDRESS="' + str(tunnel['tunnel_remote_inner_address'].ip) + '"\n')
                           interfaceFile.write('TUNNEL_INNER_PREFIX='          + str(tunnel['tunnel_local_inner_address'].network.prefixlen) + '\n\n')

                           # ====== Tunnel enabled? =========================
                           if ((localSite['site_enabled'] == False) or
                               (remoteSite['site_enabled'] == False)):
                              interfaceFile.write('TUNNEL_IS_ENABLED=0\n')
                           else:
                              interfaceFile.write('TUNNEL_IS_ENABLED=1\n')

                           # ====== Tunnel MTU ==============================
                           tunnelExternalMTU = min(localProvider['provider_mtu'], remoteProvider['provider_mtu'])
                           tunnelMTU = tunnelExternalMTU - tunnelOverhead
                           interfaceFile.write('TUNNEL_MTU=' + str(tunnelMTU) + '\n\n')

                           # ====== Routing =================================
                           interfaceFile.write('REMOTE_SITE_NETWORK="'   + str(remoteNetwork)   + '"\n')
                           interfaceFile.write('REMOTE_SITE_TUNNELBOX="' + str(remoteTunnelbox) + '"\n')
                           if ((localProviderIndex  == localSite['site_default_provider_index']) and
                               (remoteProviderIndex == remoteSite['site_default_provider_index'])):
                              metric = 5
                           else:
                              metric = 10 + pathNumber
                           interfaceFile.write('REMOTE_SITE_METRIC=' + str(metric)                 + '\n')
                           interfaceFile.write('ROUTING_TABLE='      + str(internalRoutingTableID) + '\n')

                           #print(localSite['site_utf8'], '/', localProvider['provider_long_name'], ' -> ',
                           #      remoteSite['site_utf8'], '/', remoteProvider['provider_long_name'], ': metric ',
                           #      str(metric))

                           # ====== Default route ===========================
                           # The local provider's default route to the
                           # Central Site's default provider interface
                           if ((remoteSiteIndex == NorNet_SiteIndex_Central) and
                               (localSiteIndex  != NorNet_SiteIndex_Central) and
                               (remoteProviderIndex == remoteSite['site_default_provider_index'])):
                              interfaceFile.write('TUNNEL_IS_DEFAULT_ROUTE=1\n')

                           # ====== Static routes to tunnels between remote sites ======
                           if ((localProviderIndex == localSite['site_default_provider_index']) and
                               (remoteProviderIndex == remoteSite['site_default_provider_index'])):
                              additionalStaticRoutes = makeRemoteSiteTunnelRoutes(
                                 fullSiteList, localSite, version,
                                 tunnel['tunnel_interface'], tunnel['tunnel_remote_inner_address'].ip,
                                 remoteSite)
                              if len(additionalStaticRoutes) > 0:
                                 interfaceFile.write("ADDITIONAL_STATIC_ROUTES=\"")
                                 firstEntry = True
                                 for additionalStaticRoute in additionalStaticRoutes:
                                     if firstEntry == True:
                                        firstEntry = False
                                     else:
                                        interfaceFile.write(' ')
                                     interfaceFile.write(str(additionalStaticRoute))
                                 interfaceFile.write("\"\n")

            pathNumber = pathNumber + 1


   # ====== All providers at the site =======================================
   tunnelboxFile.write('TUNNELBOX_AVAILABLE_PROVIDERS="')
   firstProvider = True
   for providerName in providerSet:
      if firstProvider == True:
         tunnelboxFile.write(providerName)
         firstProvider = False
      else:
         tunnelboxFile.write(',' + providerName)
   tunnelboxFile.write('"\n')
   tunnelboxFile.close()


# ###### Generate collectd ping configuration ###############################
def makeCollectdPingConfig():
   log('Making collectd tunnelbox ping configuration for ' + localSite['site_long_name'] + ' ...')
   localSiteIndex = localSite['site_index']
   localProviderList = getNorNetProvidersForSite(localSite)
   collectdPingGeneralConfigFileName = "collectd-tb-ping-general-config"
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]
      for version in [ 4, 6 ]:
         localProviderNetwork = makeNorNetIP(localProviderIndex, localSiteIndex, 0, version)
         tunnelboxAddress = makeNorNetIP(localProviderIndex, localSiteIndex, NorNet_NodeIndex_Tunnelbox, version)
         collectdPingConfigFileName = "collectd-tb-ping-src-" + str(tunnelboxAddress.ip)
         collectdPingConfigFile = codecs.open(collectdPingConfigFileName, 'w', 'utf-8')
         collectdPingConfigFile.write('Include "' + collectdPingGeneralConfigFileName + "\"\n")
         collectdPingConfigFile.write("<Plugin ping>\n")
         collectdPingConfigFile.write('SourceAddress "' + str(tunnelboxAddress.ip) + '"\n')
         for remoteSiteIndex in fullSiteList:
            remoteSite = fullSiteList[remoteSiteIndex]
            if remoteSite['site_enabled'] == False:
               continue
            if remoteSiteIndex == localSiteIndex:
               continue
            remoteProviderList = getNorNetProvidersForSite(remoteSite)
            for remoteProviderIndex in remoteProviderList:
              remoteProvider = remoteProviderList[remoteProviderIndex]
              remoteProviderList = getNorNetProvidersForSite(remoteSite)
              address = makeNorNetIP(remoteProviderIndex, remoteSiteIndex, NorNet_NodeIndex_Tunnelbox, version)
              collectdPingConfigFile.write('Host "' + str(address.ip) + '"\n')
         collectdPingConfigFile.write("</Plugin>\n")
         collectdPingConfigFile.write("""
PreCacheChain "ping_rewrite"
LoadPlugin match_regex
LoadPlugin target_set

<Chain "ping_rewrite">
  <Rule "set_plugin_instance">
    <Match "regex">
      Plugin "ping"
    </Match>
    <Target "set">
""")
         collectdPingConfigFile.write('      PluginInstance "src.' + str(tunnelboxAddress.ip) + '"\n')
         collectdPingConfigFile.write("""    </Target>
    Target "write"
  </Rule>
</Chain>
""")
         collectdPingConfigFile.close()


# ###########################################################################
# #### HiPerConTracer Configuration                                      ####
# ###########################################################################


# ###### Generate HiPerConTracer daemon configuration #######################
def makeHiPerConTracerConfiguration(localSite):
   localSiteIndex = localSite['site_index']
   localProviderList = getNorNetProvidersForSite(localSite)

   log('Making HiPerConTracer configuration for ' + localSite['site_long_name'] + ' ...')
   outputFile = codecs.open('hipercontracer-config', 'w', 'utf-8')

   # ====== Sources =========================================================
   outputFile.write('\n# ' + localSite['site_utf8'] + ' (local addresses):\n')
   outputFile.write('SOURCE_ARGS=""\n')
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]

      addressV4 = IPv4Interface(localProvider['provider_tunnelbox_ipv4'])
      addressV6 = IPv6Interface(localProvider['provider_tunnelbox_ipv6'])

      outputFile.write('SOURCE_ARGS="$SOURCE_ARGS -source=' + str(addressV4.ip))
      if addressV6 != IPv6Interface('::/0'):
         outputFile.write(' -source=' + str(addressV6.ip))
      outputFile.write('"\n')
   outputFile.write('\n')

   # ====== Ping/Traceroute destinations ====================================
   outputFile.write('PINGTRACEROUTE_ARGS=""\n')
   for remoteSiteIndex in fullSiteList:
      if remoteSiteIndex == localSiteIndex:
         continue
      remoteSite         = fullSiteList[remoteSiteIndex]
      remoteProviderList = getNorNetProvidersForSite(remoteSite)

      outputFile.write('\n# ' + remoteSite['site_utf8'] + ':\n')

      for remoteProviderIndex in remoteProviderList:
         remoteProvider = remoteProviderList[remoteProviderIndex]

         addressV4 = IPv4Interface(remoteProvider['provider_tunnelbox_ipv4'])
         addressV6 = IPv6Interface(remoteProvider['provider_tunnelbox_ipv6'])

         outputFile.write('PINGTRACEROUTE_ARGS="$PINGTRACEROUTE_ARGS -destination=' + str(addressV4.ip))
         if addressV6 != IPv6Interface('::/0'):
            outputFile.write(' -destination=' + str(addressV6.ip))
         outputFile.write('"\n')

   # ====== "Critical Services" destinations ================================
   outputFile.write('\n\n# Critical Services:\n')
   outputFile.write('CRITSERVICES_ARGS=""\n')
   for criticalService in [ '128.39.37.179' ]:
      outputFile.write('CRITSERVICES_ARGS="$CRITSERVICES_ARGS -destination=' + criticalService + '"\n')

   # ====== Daemon parameters ===============================================
   outputFile.write('\n\n# HiPerConTracer Daemons:\n')
   outputFile.write('HIPERCONTRACER_PINGTRACEROUTE_ARGS="-user=hipercontracer -ping -traceroute ' +
                    '-resultstransactionlength=900 -resultsdirectory=/var/hipercontracer -quiet ' +
                    '-pinginterval=1000 -pingexpiration=5000 -pingttl=64 ' +
                    '-tracerouterounds=3 -tracerouteinterval=300000 -tracerouteduration=2000 -tracerouteinitialmaxttl=5 -traceroutefinalmaxttl=35 -tracerouteincrementmaxttl=5 ' +
                    '$SOURCE_ARGS $PINGTRACEROUTE_ARGS"\n')
   outputFile.write('HIPERCONTRACER_CRITSERVICES_ARGS="-user=hipercontracer -ping -traceroute ' +
                    '-resultstransactionlength=900 -resultsdirectory=/var/hipercontracer -quiet ' +
                    '-pinginterval=60000 -pingexpiration=5000 -pingttl=64 '
                    '-tracerouterounds=3 -tracerouteinterval=3600000 -tracerouteduration=2000 -tracerouteinitialmaxttl=5 -traceroutefinalmaxttl=35 -tracerouteincrementmaxttl=5' +
                    '$SOURCE_ARGS $CRITSERVICES_ARGS"\n')

   outputFile.close()


# ###########################################################################
# #### Fling Configuration                                               ####
# ###########################################################################


# ###### Generate Fling daemon configuration ################################
def makeFlingConfiguration(localSite):
   localProviderList = getNorNetProvidersForSite(localSite)

   log('Making Fling configuration for ' + localSite['site_long_name'] + ' ...')
   outputFile = codecs.open('fling-config', 'w', 'utf-8')

   outputFile.write('DAEMON_ARGS="$DAEMON_ARGS -H 77.88.71.145 -T 4 -D 30 -i')
   for localProviderIndex in localProviderList:
      localProvider = localProviderList[localProviderIndex]
      outputFile.write(' ' + localProvider['provider_tunnelbox_interface'])
   outputFile.write('"\n')

   outputFile.close()


   log('Making Fling Server configuration for ' + localSite['site_long_name'] + ' ...')
   outputFile = codecs.open('fling-server-config', 'w', 'utf-8')

   outputFile.write('DAEMON_ARGS="$DAEMON_ARGS -H 77.88.71.145 -a tcp -i')
   for localProviderOnlyDefault in [ True, False ]:
      for localProviderIndex in localProviderList:
         if ( ((localProviderOnlyDefault == True)  and (localProviderIndex == localSite['site_default_provider_index'])) or \
              ((localProviderOnlyDefault == False) and (localProviderIndex != localSite['site_default_provider_index'])) ):

            localProvider = localProviderList[localProviderIndex]
            outputFile.write(' ' + localProvider['provider_tunnelbox_interface'])

   outputFile.write('"\n')

   outputFile.close()



# ###########################################################################
# #### Main program                                                      ####
# ###########################################################################


# ====== Get configuration ==================================================
try:
   loginToPLC()
except Exception as e:
   sys.stderr.write('ERROR: ' + str(e) + '!\n')
   sys.exit(1)
fullSiteList = fetchNorNetSiteList(False)
localSite    = None
try:
   localSite = fullSiteList[getLocalSiteIndex()]
except:
   error('Cannot find local site ' + str(getLocalSiteIndex()) + ' at PLC database!')
fullSliceList = fetchNorNetSliceList()
siteNodeList  = fetchNorNetNodeListForSite(localSite)
if siteNodeList == None:
   siteNodeList = []

# ====== Some checks ========================================================
if getLocalSiteIndex() == None:
   error('NorNet_LocalSite_SiteIndex has not been set in configuration file!')
if ((getLocalSiteIndex() < NorNet_MinSiteIndex) or (getLocalSiteIndex() > NorNet_MaxSiteIndex)):
    error('Bad NorNet_LocalSite_SiteIndex ' + str(getLocalSiteIndex()))
if getLocalSiteDefaultProviderIndex() == None:
   error('NorNet_LocalSite_DefaultProviderIndex has not been set in configuration file!')
if ((getLocalSiteDefaultProviderIndex() < NorNet_MinProviderIndex) or (getLocalSiteDefaultProviderIndex() > NorNet_MaxProviderIndex)):
    error('Bad NorNet_LocalSite_DefaultProviderIndex ' + str(getLocalSiteDefaultProviderIndex()))
if getLocalSiteTunnelboxDefaultProviderIPv4() == None:
   error('NorNet_LocalSite_TBDefaultProviderIPv4 has not been set in configuration file!')
if getLocalNodeNorNetInterface() == None:
   error('NorNet_LocalNode_NorNetInterface has not been set in configuration file!')


# ====== Tunnelbox configuration ============================================
makeTunnelboxConfiguration(fullSiteList, localSite)


# ====== Prepare additional nodes for DNS configuration =====================
localHostName   = getHostnameFromFQDN(getLocalNodeHostname())
additionalNodes = [ makeNodeForDNS(localHostName,
                                   localSite, NorNet_NodeIndex_Tunnelbox,
                                   'Amiga 6000', 'NorNet Tunnelbox', []) ]
aliases = {}
sshKeys = {}
for i in range(NorNet_MinNodeIndex, NorNet_MaxNodeIndex + 1):
   # ====== Handle nodes ====================================================
   nodeConfiguration = getLocalNodeConfigurationString(i)
   if nodeConfiguration != '':
      # print(i, nodeConfiguration)
      try:
         parameters = shlex.split(nodeConfiguration)
      except:
         parameters = []
      if len(parameters) < 3:
         error('Bad configuration <' + nodeConfiguration + '> for NorNet_LocalSite_Node' + str(i))

      nodeName     = unquote(parameters[0])
      hardwareInfo = unquote(parameters[1])
      softwareInfo = unquote(parameters[2])
      try:
         options = tuple(unquote(parameters[3]).split(','))
      except:
         options = []
      additionalNodes.append(makeNodeForDNS(nodeName, localSite, i, hardwareInfo, softwareInfo, options))

   # ====== Handle aliases ==================================================
   nodeAliases = getLocalNodeAliasesString(i)
   if nodeAliases != '':
      parameters = shlex.split(nodeAliases)
      if len(parameters) > 0:
         aliases[i] = parameters

   # ====== Handle SSH public keys ==========================================
   nodeSSHKeys = getLocalNodeSSHKeysString(i)
   if nodeSSHKeys != '':
      parameters = shlex.split(nodeSSHKeys)
      if len(parameters) > 0:
         sshKeys[i] = parameters


# ====== Handle slices ======================================================
if fullSliceList != None:
   for slice in fullSliceList:
      for node in siteNodeList:
         if node['node_id'] in slice['slice_node_ids']:
            sliceDNSName   = str.replace(slice['slice_name'], '_', '-')
            sliceHostname  = sliceDNSName + '.' + getHostnameFromFQDN(node['node_utf8'])
            sliceNodeIndex = getSliceNodeIndexOfNorNetSlice(slice, node)
            sliceNode = makeNodeForDNS(sliceHostname,
                                       getNorNetSiteOfNode(fullSiteList, node),
                                       node['node_index'],
                                       node['node_model'], 'Slice ' + slice['slice_name'],
                                       [ 'slice' ])
            # Add a pseudo entry to create a slice address later
            sliceNode['pseudo_slice_node_index'] = getSliceNodeIndexOfNorNetSlice(slice, node)
            sliceNode['node_ssh_rsa_key']        = node['node_ssh_rsa_key']
            additionalNodes.append(sliceNode)
else:
   log('WARNING: There are no slices! Is the user role correct?')

# ====== Set SSH public keys ================================================
for additionalNode in additionalNodes:
   additionalNodeIndex = additionalNode['node_index']
   try:
      additionalNode['node_ssh_rsa_key'] = sshKeys[additionalNodeIndex]
   except:
      continue


# ====== DHCP daemon configuration ==========================================
dynamicStart         = 0
dynamicEnd           = 0
dynamicConfiguration = getLocalSiteDHCPServerDynamicConfigurationString()
dynamicNodeNames     = shlex.split(getLocalSiteDHCPServerDynamicNodeNamesString())
if ((dynamicConfiguration != None) and (dynamicConfiguration != '')):
   parameters = shlex.split(dynamicConfiguration)
   if len(parameters) < 2:
      error('Bad configuration "' + dynamicConfiguration + '" for NorNet_LocalSite_DHCPServer_DynamicRange')
   try:
      dynamicStart = int(unquote(parameters[0]))
      dynamicEnd   = int(unquote(parameters[1]))
   except:
      error('Bad configuration "' + dynamicConfiguration + '" for NorNet_LocalSite_DHCPServer_DynamicRange node range values')
   if ((dynamicStart <= NorNet_MinNodeIndex) or (dynamicStart > NorNet_MaxNodeIndex) or
       (dynamicEnd <= NorNet_MinNodeIndex) or (dynamicEnd > NorNet_MaxNodeIndex)):
      error('Bad configuration "' + dynamicConfiguration + '" for NorNet_LocalSite_DHCPServer_DynamicRange node range indexes')

   j = 1
   for i in range(dynamicStart, dynamicEnd + 1):
      simpleDhcpName = 'dhcp{:02d}'.format(j)
      try:
         dhcpName = dynamicNodeNames[j - 1]
      except:
         dhcpName = simpleDhcpName

      if i in aliases:
         error('Alias set for DHCP entry ' + str(i))
      if getLocalNodeConfigurationString(i) != '':
         error('Node set for DHCP entry ' + str(i))

      # print("D=",i - dynamicStart,dhcpName)
      additionalNodes.append(makeNodeForDNS(dhcpName, localSite, i,
                                            'NorNet Node', 'DHCP, dynamic entry ' + str(j) + '/' + str(dynamicEnd + 1 - dynamicStart),
                                            [ 'def-v4only' ]))
      if dhcpName != simpleDhcpName:
         aliases[i] = [ simpleDhcpName ]
      j = j + 1


staticList = []
for i in range(NorNet_MinNodeIndex + 1, NorNet_MaxNodeIndex + 1):
   staticConfiguration = getLocalSiteDHCPServerStaticConfigurationString(i)
   if staticConfiguration != '':
      # print i, staticConfiguration
      parameters = shlex.split(staticConfiguration)
      if len(parameters) < 1:
         error('Bad configuration "' + staticConfiguration + '" for NorNet_LocalSite_DHCPServer_Static' + str(i))

      mac = unquote(parameters[0])
      try:
         fileName = unquote(parameters[1])
      except:
         fileName = '';
      try:
         serverName = unquote(parameters[2])
      except:
         serverName = '';

      staticEntry = {
         'node_index'      : i,
         'node_mac'        : mac,
         'node_filename'   : fileName,
         'node_servername' : serverName
      }
      staticList.append(staticEntry)

denyList = []
denyConfiguration = getLocalSiteDHCPServerDenyString()
for denyEntry in shlex.split(denyConfiguration):
   print(denyEntry)
   denyList.append(unquote(denyEntry))

# ====== Create Bind configuration ==========================================
makeBindConfiguration(fullSiteList, siteNodeList, fullSliceList, localSite,
                      localHostName, additionalNodes, aliases)

# ====== Create other configurations ========================================
makeHiPerConTracerConfiguration(localSite)
makeFlingConfiguration(localSite)
#makeIPSECConfiguration(localSite)
makeDHCPDConfiguration(localSite, dynamicStart, dynamicEnd, staticList, denyList)
makeRADVDConfiguration(localSite)
makeSquidConfiguration(getLocalNodeHostname(), localSite)
makeRegistrarConfiguration(localSite)

# ====== Collectd configration ==========================================
makeCollectdPingConfig()
