Mettre à jour automatiquement ses domaines sur le DNS secondaire d’OVH grâce à l’API

30 Déc

J’avais déjà eu l’occasion d’en parler sur ce blog, j’utilise un script pour mettre à jour automatiquement le DNS secondaire fourni par OVH.

Le principe est simple : j’héberge moi-même mes serveurs DNS avec Bind9. Pour éviter tout problème j’utilise en réplique de ces DNS le service de DNS secondaire mis gratuitement à disposition par OVH pour tout possesseur d’un serveur dédié (de la gamme OVH, SoYouStart ou Kimsufi) ou d’un VPS.

Traditionnellement, il faut penser à réaliser l’opération manuellement, autrement dit, si vous ajoutez un domaine sur votre serveur DNS Bind9 il faut penser à aller le faire sur le Manager d’OVH.

J’avais trouvé sur le net un script qui utilise l’ancienne API d’OVH (soApi) pour automatiser l’opération en scannant de manière régulière les zones paramétrées sur Bind9 et celles paramétrées sur le DNS secondaire d’OVH.

J’ai modifié légèrement cet outil et j’avais décrit les modalités de mise en place de cette solution.

Le hic, car il y a un hic, c’est que soApi va prochainement être abandonné par OVH. Il fallait donc faire les modifications sur le script pour l’adapter à l’API v6 d’OVH. Pour faciliter la migration, un tableau des équivalences en ligne a été mis à disposition par OVH. Les développements ont été réalisés par Romain Montagne, de Supvize.com, car je ne suis pas un super champion en PHP. Merci à lui pour la qualité du boulot !

Voici donc un petit guide pour mettre en place cette solution.

Etape 1 : récupérer le script et le fichier de configuration

Pour installer lancez les commandes suivantes :

cd ~
git clone https://github.com/yvangodard/ovh-dnsupdater.git
cd ovh-dnsupdater
cp etc/ovh-dnsupdater /etc/ovh-dnsupdater
chmod 640 /etc/ovh-dnsupdater
cp ovh-dnsupdater /usr/sbin/ovh-dnsupdater
chmod 755 /usr/sbin/ovh-dnsupdater
mkdir /var/lib/ovh-dnsupdater
chmod 750 /var/lib/ovh-dnsupdater
cd ..
rm -R ovh-dnsupdater

L’outil sera donc installé à l’emplacement /usr/sbin/ovh-dnsupdater et le fichier de configuration sera /etc/ovh-dnsupdater.

Etape 2 : créer un token d’accès sur l’API et configurer ovh-dnsupdater

Rendez-vous à l’adresse https://eu.api.ovh.com/createApp/ pour vous ouvrir un accès à l’API.

API

Munissez-vous de votre NIC et de votre mot de passe, donnez un nom à cette App, par exemple OVH_DNS_Updater et validez.

Récupérez l’Application Key et l’Application Secret après avoir cliqué sur le bouton « Create Keys » et reportez ces informations en éditant le fichier de configuration /etc/ovh-dnsupdater (ce sont les lignes acces_key et acces_secret).

Modifiez aussi la section [server] du fichier de configuration en renseignant :

  • type : mettez vps si vous utilisez le DNS secondaire d’un VPS, mettez dedicated si vous utilisez le DNS secondaire d’un serveur dédié
  • hostname : mettez le nom de la machine sur laquelle tourne le script, tel qu’il apparait dans le manager d’OVH
  • ip : reportez l’IP du serveur sur lequel tourne le script.

Modifiez enfin la section [paths] du fichier de configuration en cohérence avec votre configuration Bind9. Pour ma part :

  • zones = /var/lib/bind
  • data = /var/lib/ovh-dnsupdater (à ne pas modifier, c’est un répertoire « tampon » utilisé par le script)
  • zone_suffix = .hosts

Une fois que vous avez reporté ces informations, il vous faut lancer une première fois le script avec l’option --consumer-key pour générer une « consumer_key » :

/usr/sbin/ovh-dnsupdater --consumer-key

La console va vous renvoyer un information du type :

1. For validate your consumer_key, connect to : https://eu.api.ovh.com/auth/?credentialToken=lPE68uE7Swctg7dI2bBG8gSOx3GtMQmy NkM3Bgx0J6DpWUUDHCHQWRPZ8vutOI0c
2. Insert your consumer_key on config file : KNBGo0Ufphd8cs0sv0hmeMKJMujTdeOo

Copiez l’URL renvoyée au point 1. et ouvrez-là dans votre navigateur favori. Rentrez à nouveau vos information de connexion OVH et mettez bien la « Validity » sur « Unlimited » pour que votre accès l’API ne soit pas limité dans le temps.

API Access

Ne tenez pas compte de la page d’erreur qui vous sera sans doute retournée après avoir validé la consumer_key. Cela n’empêche pas le process de fonctionner.

Copiez ensuite la consumer_key qui vous a été retournée dans le fichier de configuration /etc/ovh-dnsupdater (c’est la ligne consumer_key de la section [api]).

Attention, important : si vous utilisez cet outil …

  • sur un serveur de la Gamme Kimsufi qui s’administre depuis https://www.kimsufi.com/fr/manager il faudra, avant de réaliser la configuration, modifier la ligne url du fichier de configuration ainsi : url = https://eu.api.kimsufi.com/1.0,
    il faudra enfin créer votre accès à l’API depuis l’URL https://eu.api.kimsufi.com/createApp/.
    Pour info, les serveurs de la gamme Kimsufi d’il y a plusieurs années ne s’administrent pas depuis l’interface Kimsufi et utilisent l’API standard d’OVH.
  • sur un serveur de la Gamme SoYouStart qui s’administre depuis https://eu.soyoustart.com/manager il faudra, avant de réaliser la configuration, modifier la ligne url du fichier de configuration ainsi : url = https://eu.api.soyoustart.com/1.0,
    il faudra enfin créer votre accès à l’API depuis l’URL https://eu.api.soyoustart.com/createApp/.

Etape 3 : Comprendre l’usage l’outil ovh-dnsupdater

Le script s’utilise avec la syntaxe suivante : ./ovh-dnsupdater [options]

Où les paramètres sont :

  • --conf-file=<file> : charge un fichier de configuration <file> spécifique à la place de /etc/ovh-dnsupdater
  • --dry-run : permet de lancer le script sans appliquer les modifications. Cela permet des tests.
  • -h, --help : affiche l’aide.
  • --hard : force la récupération de la liste des domaines déjà enregistrés sur le service de DNS secondaire en utilisant l’API et les enregistre en cache sur le serveur,
  • --consumer-key : génère une consumer_key et l’URL de validation du token associée.

Ovh-dnsupdater fonctionne en surveillant le répertoire de la Zone Bind sur votre serveur et compare avec ce qui est enregistré chez OVH. A chaque fois qu’il est lancé cet outil va donc :

  1. Récupérer la liste des domaines enregistrés chez OVH sur le service de DNS secondaire
  2. Récupérer la liste des domaines configurés localement (sur Bind9)
  3. Calculer la différence entre ces listes
  4. Ajouter ou supprimer chez OVH sur le service de DNS secondaire les domaines afin que les deux listes soient synchronisées.

La seule chose à bien comprendre est l’usage de l’option --hard. Quand elle est utilisée, le script va d’abord télécharger la liste des domaines déjà enregistrés sur le service de DNS secondaire en utilisant l’API et les stocker dans un cache local, avant de poursuivre. Sans l’usage de cette option, le script comparera les domaines enregistrés localement avec la version en cache.

Pour rester léger, l’idée sera donc d’alterner des appels via cron sans l’usage de l’option --hard (toutes les 5 ou 10 minutes) et de manière moins régulière.

Nous allons donc ajouter ces lignes dans la table Cron (en utilisant la commande crontab -e) :

# lancement de ovh-dnsupdater
*/5 *   *   *   *  root /usr/sbin/ovh-dnsupdater # lancement toutes les 5 minutes
7   2   *   *   *  root /usr/sbin/ovh-dnsupdater --hard # lancement 1 fois par jour

Etape 4 : Tester l’outil ovh-dnsupdater

Evidemment, avant de considérer que tout est OK, il vaut mieux faire un petit test. Pour cela, lancez :

/usr/sbin/ovh-dnsupdater --hard --dry-run

puis allez regarder le résultat dans le journal :

tail -1000 /var/log/syslog | grep ovh-dnsupdater

… vous devriez y trouver une trace des modifications à appliquer.

Si tout est OK, lancez en « vrai » la commande :

/usr/sbin/ovh-dnsupdater --hard

… et vérifiez sur la Manager OVH que les modifications ont été correctement appliquées.

Attention, pour certains domaines, OVH vous demandera d’entrer une entrée TXT « ownercheck » dans la zone concernée pour accepter l’usage de l’API. Vous trouverez une information du type :

Dec 27 21:39:30 dedie ovh-dnsupdater[2894]: Failed to add domain mondomainefavori.ovh to the secondary DNS server: First we need to verify you are the owner of this domain. To do so, please add a TXT field on your DNS zone for the domain mondomainefavori.ovh, with the subdomain 'ownercheck' and the following value: 'p1nm9wff'. Once done and your zone reloaded, try again (you don't need to wait for DNS propagation).

Il suffit de créer cette entrée dans votre DNS et relancer le script en scrutant vos logs pour voir si la modification est correctement appliquée. Pour la suite, je vous recommande d’ajouter dans toutes vos zones cette entrée pour le sous-domaine ownercheck avec la clé fourni qui permettra d’identifier que vous êtes bien habilité à travailler sur ce domaine depuis l’API.

Pour finir : les tutoriels d’OVH et la documentation

J’en profite aussi pour vous signaler quelques tutoriels simples mis en place par OVH :

Et la documentation complète du script : https://github.com/yvangodard/ovh-dnsupdater

Pour les curieux, le code source PHP :

#!/usr/bin/php
<?php
/* 
ovh-dnsupdater
Version 2.0

Original script by Marc PUJOL - http://www.mendeley.com/profiles/marc-pujol-gonzalez
Modded by Yvan GODARD - godardyvan@gmail.com - http://www.yvangodard.me
Modded to work with OVH new API (https://api.ovh.com/) by Romain MONTAGNE - http://www.supvize.com

Ovh-dnsupdater is a small utility to automate the addition and removal of domains 
to the (free) secondary DNS server offered by OVH.

Readme : https://github.com/yvangodard/ovh-dnsupdater

THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

This program is distributed under the terms of Creative Commons BY NC SA 4.0 :
http://creativecommons.org/licenses/by-nc-sa/4.0

*/

// Parse user-specified options
$programName = array_shift($argv);
$options = array(
	'hard'      => FALSE,
	'h'         => FALSE,
	'help'      => FALSE,
	'dry-run'   => FALSE,
	'consumer-key'   => FALSE,
	'conf-file' => '/etc/ovh-dnsupdater',
);
foreach ($argv as $arg) {
	$arg = trim($arg, '- ');

	$value = TRUE;
	$pos = strrpos($arg, '=');
	if ($pos !== FALSE) {
		$value = substr($arg, $pos+1);
		$arg   = substr($arg, 0, $pos);
	}
	$options[$arg] = $value;
}

if ($options['help']) {
	echo <<<EOD
Usage: 
  $programName [options]
Options:
  --conf-file=<file>
	Load the configuration from file <file> instead of /etc/ovh-dnsupdater
  --dry-run    
	Just show what would be done, without actually doing anything.
  -h, --help   
	Show this help message.
  --hard       
	Force downloading the list of already setup domains using the API.
  --consumer-key      
	Generate a consumer_key.

EOD;
	exit(0);
}

// Load the configuration file
$conf = parse_ini_file($options['conf-file'], true);
$conf['paths']['setup_domains'] = $conf['paths']['data'] . '/domains';

$updater = new DnsUpdater($conf['api']['url'], $conf['api']['acces_key'], $conf['api']['acces_secret'], $conf['api']['consumer_key'],
	$conf['server']['type'], $conf['server']['hostname'], $conf['server']['ip']);

openlog($conf['syslog']['prefix'], LOG_CONS | LOG_PID, LOG_DAEMON);

if ($options['consumer-key']) {
	$return = $updater->auth('ovh.com');
	
	if ($return['http_code'] == 200) {
	    echo "1. For validate your consumer_key, connect to : ".$return['response']->validationUrl."\n";
        echo "2. Insert your consumer_key on config file : ".$return['response']->consumerKey."\n";
	} else {
        echo "An error with your ovh app, please check your config file !\n";
	}
	exit("\n");
}

if (empty($conf['api']['consumer_key'])) {
    exit("Before use this program you must obtain a consumer_key, use --consumer-key \n");
}

if ($options['dry-run']) {
	syslog(LOG_NOTICE, "Dry-run specified, the following actions will NOT be really processed.");
}

// Fetch the previous (old) list of domains already setup
if ($options['hard']) {
	$old = $updater->getDomains();
} else {
	$old = array_map("trim", file($conf['paths']['setup_domains']));
}

// Fetch the current (new) list of defined zones (domains)
if (!is_dir($conf['paths']['zones'])) {
	syslog(LOG_ERR, 'The configured path to the zones is not a directory');
	exit(1);
}
$new = array();
$fh = opendir($conf['paths']['zones']);
if (!$fh) {
	syslog(LOG_ERR, 'Unable to open the zones directory');
	exit(1);
}
while (($file = readdir($fh)) !== FALSE) {
	if ($file == '.' || $file == '..') {
		continue;
	}

	$pos = strrpos($file, $conf['paths']['zone_suffix']);
	if ($pos !== FALSE) {
		$new[] = trim(substr($file, 0, $pos));
	}
}


// Compute the differences and add/remove domains as needed
$removed = array_diff($old, $new);
$added = array_diff($new, $old);

foreach($removed as $domain) {
	$domain = trim($domain);
	try {
		if (!$options['dry-run']) {
			$updater->removeDomain($domain);
		}
		syslog(LOG_NOTICE, 'Domain ' . $domain . ' removed from the secondary DNS server');
	} catch(Exception $e) {
		syslog(LOG_NOTICE, 'Failed to remove domain ' . $domain . ' from the secondary DNS server: ' . $e->getMessage());
	}
}
foreach($added as $domain) {
	$domain = trim($domain);

	try {
		if (!$options['dry-run']) {
			$updater->addDomain($domain);
		}
		syslog(LOG_NOTICE, 'Domain ' . $domain . ' added to the secondary DNS server');
	} catch(Exception $e) {
		syslog(LOG_NOTICE, 'Failed to add domain ' . $domain . ' to the secondary DNS server: ' . $e->getMessage());
	}

}

// Save the current list of configured domains
file_put_contents($conf['paths']['setup_domains'], implode("\n", $new) . "\n");

class DnsUpdater {
	private $_url;
	private $_acces_key;
	private $_acces_secret;
    private $_consummer_key = false;
	
	private $_type;
	private $_host;
	private $_ip;
	private $_dnshost;
	private $_dnsip;

	private $_ovhapi = false;

	public function __construct($url, $acces_key, $acces_secret, $consummer_key, $type, $host, $ip) {
		$this->_url  = $url;
		$this->_acces_key = $acces_key;
		$this->_acces_secret = $acces_secret;
		if (!empty($consummer_key)) {
            $this->_consummer_key = $consummer_key;
        }
        
        $this->_type = $type;
        if (strtolower($type) != 'vps') {
            $this->_type = 'dedicated/server';  
        }
		$this->_host = $host;
		$this->_ip   = $ip;
		
        $this->_ovhapi = new OvhApi($this->_url, $this->_acces_key, $this->_acces_secret, $this->_consummer_key);
	}

    public function auth($redirect) {
        $json = array(
            "accessRules" => array(
                array(
                    "method" => "GET",
                    "path" => "/dedicated/server/*/secondaryDnsDomains",
                ),
                array(
                    "method" => "POST",
                    "path" => "/dedicated/server/*/secondaryDnsDomains",
                ),
                array(
                    "method" => "DELETE",
                    "path" => "/dedicated/server/*/secondaryDnsDomains/*",
                ),
                array(
                    "method" => "GET",
                    "path" => "/vps/*/secondaryDnsDomains",
                ),
                array(
                    "method" => "POST",
                    "path" => "/vps/*/secondaryDnsDomains",
                ),
                array(
                    "method" => "DELETE",
                    "path" => "/vps/*/secondaryDnsDomains/*",
                ),
            ),
            "redirection" => $redirect
        );

        return $this->_ovhapi->post('/auth/credential', $json);
    }

	public function addDomain($domain) {
    	
		$params = array('domain' => $domain, 'ip' => $this->_ip);
		$api_return = $this->_ovhapi->post('/'.$this->_type.'/'.$this->_host.'/secondaryDnsDomains', $params);
		if ($api_return['http_code'] == 200) {
            return true;
        } else {
           throw new Exception($api_return['response']->message);
        }
		
	}

	public function removeDomain($domain) {
 
		$api_return = $this->_ovhapi->delete('/'.$this->_type.'/'.$this->_host.'/secondaryDnsDomains/'.$domain);
		if ($api_return['http_code'] == 200) {
            return true;
        } else {
           throw new Exception($api_return['response']->message);
        }
         
	}

	public function getDomains() {
        $api_return = $this->_ovhapi->get('/'.$this->_type.'/'.$this->_host.'/secondaryDnsDomains');
        if ($api_return['http_code'] == 200) {
            return $api_return['response'];
        } else {
            return false;
        }
	}
}

class OvhApi {

    var $AK;
    var $AS;
    var $CK;
    var $timeDrift = 0;
    function __construct($_root, $_ak, $_as, $_ck) {
        // INIT vars
        $this->AK = $_ak;
        $this->AS = $_as;
        $this->CK = $_ck;
        $this->ROOT = $_root;

        // Compute time drift
        $serverTimeRequest = file_get_contents($this->ROOT . '/auth/time');
        if($serverTimeRequest !== FALSE)
        {
            $this->timeDrift = time() - (int)$serverTimeRequest;
        }
    }
    function call($method, $url, $body = NULL)
    {
        $url = $this->ROOT . $url;
        if($body)
        {
            $body = json_encode($body);
        }
        else
        {
            $body = "";
        }

        // Compute signature
        $time = time() - $this->timeDrift;
        $toSign = $this->AS.'+'.$this->CK.'+'.$method.'+'.$url.'+'.$body.'+'.$time;
        $signature = '$1$' . sha1($toSign);

        // Call
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        if ($this->CK) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, array(
                'Content-Type:application/json',
                'X-Ovh-Application:' . $this->AK,
                'X-Ovh-Consumer:' . $this->CK,
                'X-Ovh-Signature:' . $signature,
                'X-Ovh-Timestamp:' . $time,
            ));
        } else {
            curl_setopt($curl, CURLOPT_HTTPHEADER, array(
                'Content-Type:application/json',
                'X-Ovh-Application:' . $this->AK
            ));
        }
    
        
        if($body) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
        }
        $result = curl_exec($curl);
        $info = curl_getinfo($curl);
        
        if($result === FALSE) {
            return NULL;
        }

        return array('http_code' => $info['http_code'], 'response' => json_decode($result));
    }
    function get($url)
    {
        return $this->call("GET", $url);
    }
    function put($url, $body)
    {
        return $this->call("PUT", $url, $body);
    }
    function post($url, $body)
    {
        return $this->call("POST", $url, $body);
    }
    function delete($url)
    {
        return $this->call("DELETE", $url);
    }
}

 

#JeSuisCharlie