Class::DBI, un ORM pour Perl : Application à la gestion de DNS

Article publié dans Linux Magazine 96, juillet/août 2007.

Copyright © 2007 - Laurent Gautrot

[+ del.icio.us] [+ Developers Zone] [+ Bookmarks.fr] [Digg this] [+ My Yahoo!]

Chapeau

Class::DBI [1] offre une couche d'abstraction très pratique et très agréable aux bases de données. Nous allons voir en pratique comment utiliser Perl et CDBI pour gérer des zones DNS à partir d'une base de données SQLite.

Introduction

Perl permet l'utilisation de nombreuses sources de données (bases de données relationnelles ou fichiers) grâce à DBI. Sa portabilité et sa souplesse ne sont plus à démontrer.

Pour des projets d'envergure, l'appel direct à DBI peut devenir un casse-tête, en obligeant à manipuler des notions de base de données et de logique métier au sein des mêmes morceaux de code.

Les notions de persistance ne sont pas non plus au rendez-vous. Un ORM (Object-Relational Mapper) comme Class::DBI permet de se concentrer sur le métier et de laisser de côté le gros du travail de connexion aux bases de données. En particulier, il n'est pas besoin d'écrire de SQL, au moins dans 80% des cas.

Présentation de CDBI

Naturellement, CDBI (c'est son petit nom) établit des correspondances entre un modèle objet et un modèle relationnel, en donnant des méthodes (accesseurs) pour les colonnes des tables, ainsi que des moyens de naviguer entre les tables en modélisant les relations.

En fait, CDBI va même beaucoup plus loin. Certaines fonctionnalités inexistantes au niveau du SGBD peuvent être utilisées. C'est le cas des déclencheurs (triggers), du support de l'intégrité référentielle et des mécanismes afférents (suppressions ou mise à « NULL » en cascade), des transactions, ou même de petites choses toutes simples comme les auto-incréments pour des valeurs entières de clefs primaires.

Il est aussi possible de « mélanger » des SGBD, ce qui est très pratique lors de migrations progressives, avec certains morceaux du SI qui utilisent une technologie et d'autres morceaux qui en utilisent une autre.

Ce transfert de compétences au niveau applicatif apporte une grande souplesse et une portabilité, qui peut toutefois s'exercer au détriment de la performance.

Il existe bien sûr plusieurs autres ORM dans le monde Perl, parmi lesquels on peut citer DBIx::Class, Tangram, Alzabo, Jifty::DBI. Certains comme Tangram ou Alzabo sont assez anciens (les premières versions de Tangram datent de 1999, celles d'Alzabo de 2000), et donc stables et matures. Class::DBI lui-même date de fin 2001, mais par rapport aux précédents s'est rapidement orienté sur une approche très modulaire, en favorisant l'utilisation de plugins.

Plus récent, DBIx::Class est apparu il y a deux ans pour répondre aux besoins des développeurs de Catalyst (le framework web agile en Perl, comparable à RoR) qui ne pouvaient être satisfaits par Class:DBI. Quoiqu'avec une orientation différente, DBIx::Class en reste assez proche sur certains points dont une partie de l'API, et inclut d'ailleurs la suite de tests de Class::DBI. Encore plus récent, Jifty::DBI est l'ORM utilisé par Jifty (un nouveau framework web agile).

Prérequis

Comme son nom l'indique (au moins, un peu, reconnaissez-le), CDBI s'appuie sur DBI pour s'interfacer avec les bases de données. En fonction de ce que vous prévoyez d'utiliser comme SGBD, il est judicieux d'installer le ou les pilotes (DBD : DataBase Driver) correspondant à votre système : DBD::Pg, DBD::MySQL, DBD::SQLite, etc.

Dans les exemples qui suivent, le SGDB utilisé est SQLite. Le module DBD::SQLite fournit à DBI un pilote pour les bases de données de ce type. Il contient un moteur SQLite (domaine public), et il n'est donc pas nécessaire d'installer un autre moteur.

Principe d'utilisation

Les exemples classiques tirés de la documentation de CDBI (et de DBIx::Class, entre autres) traitent de la base de données de gestion de discothèque. Seul Alzabo joue la carte de la dissidence en proposant la gestion de vidéothèque, mais je m'égare.

La correspondance entre le modèle objet et le modèle relationnel est très simple, voire simpliste, puisqu'une table de la base correspond à une classe.

La première étape consiste à choisir un espace de nommage pour les classes que nous allons décrire.

En général, on considère que c'est une bonne pratique de définir une classe qui hérite de CDBI, puis de créer des classes qui héritent de cette classe. On peut alors préciser des paramétrages au niveau de cette classe « racine » (valeurs d'AutoCommit, etc.). Les autres classes du programme héritent ensuite de cette classe, en bénéficiant des paramètres ou en les surchargeant en cas de besoin, ce qui permet de ne décrire la connexion à la base de données qu'une seule fois. Toutefois, si plusieurs bases de données sont impliquées, il suffit de surcharger la connexion à la base de données.

Ensuite, à l'aide de méthodes bien nommées, on paramètre la table (méthode table()) correspondant à une classe, les colonnes (columns()) de cette table et les relations qui peuvent exister avec d'autres tables (has_a(), has_many(), might_have()). Bien entendu, si des relations particulières existent et ne sont pas prévues, il est possible de les développer et d'enrichir CDBI. Contributeurs bienvenus.

Toutes les autres définitions (déclencheurs, auto-incréments, etc.) sont aussi définies à ce niveau.

La déclaration de la classe rend disponibles des méthodes nommées de manière prévisible pour accéder aux valeurs des colonnes d'une table ou des tables qui lui sont liées.

Si j'ai une table « zone », qui contient la liste des zones que je gère dans mon DNS, je peux utiliser la requête SQL suivante :

   CREATE TABLE zone (
      id       INTEGER PRIMARY KEY,
      zone     VARCHAR(255),
      internal INTEGER NOT NULL DEFAULT 0
   );

Et une connexion CDBI s'écrirait de cette manière :

    package DNS::Manager::DBI;
    use base 'Class::DBI';
    DNS::Manager::DBI->connection('dbi:SQLite:dbname=dns.db', '', '');

    package DNS::Manager::zone;
    use base 'DNS::Manager::DBI';
    DNS::Manager::zone->table('zone');
    DNS::Manager::zone->columns(All => qw/id zone/);

Contexte de l'application exemple

Pour les administrateurs systèmes qui gèrent des DNS, la fabrication des fichiers de zone (les bases de données à proprement parler) « à la main » peut s'avérer fastidieuse, voire périlleuse.

Quoi de plus rageant qu'un caractère parasite introduit par une intervention humaine et indétectable pour qui n'est pas une machine ? Certes, il existe des outils pour valider la syntaxe des fichiers de zone. Néanmoins, certaines erreurs peuvent être très pénibles à détecter et corriger, si elles ne relèvent pas de la syntaxe.

Fichiers de zone

Les fichiers de zone constituent un format parfaitement défini (qui est aussi celui des requêtes DNS) et relativement lisible pour un sysadmin. Étant des fichiers texte, ils ne nécessitent qu'un simple éditeur de texte (vi, emacs, autre) pour être modifiés et peuvent facilement être gérés dans un SCM (SCCS, RCS, CVS, SVN, autre).

Utiliser une base de données offre en effet un certain confort lorsqu'on est amené à gérer plusieurs centaines d'enregistrements, mais cela nécessite aussi de disposer d'une interface graphique ou web permettant de gérer les entrées. Par ailleurs, cela impose de repenser/recréer un système de gestion des versions.

Nous envisageons donc une application avec un jeu de commandes s'appuyant sur une base de données assez simple. Cette dernière contient les correspondances A-Record (Nom<->IP) et CNAME (Nom -> Nom).

Pour cela, nous allons avoir dans un premier temps un schéma constitué de cinq tables. C'est un schéma « simple », mais qui contient deux petites subtilités dans la mise en œuvre.

Ce petit projet devrait offrir :

Schéma

Les relations entre associations se traduisent par les références suivantes :

Et le code SQL résultant serait le suivant :

   CREATE TABLE machine (
      id                INTEGER      PRIMARY KEY AUTOINCREMENT,
      machine           VARCHAR(50)  UNIQUE NOT NULL DEFAULT '',
      visible           INTEGER             NOT NULL DEFAULT 1
   );

   CREATE TABLE zone (
      id                INTEGER      PRIMARY KEY AUTOINCREMENT,
      zone              VARCHAR(255) UNIQUE NOT NULL DEFAULT '',
      managed           INTEGER             NOT NULL DEFAULT 0
   );

   CREATE TABLE alias (
      alias             VARCHAR(50)         NOT NULL DEFAULT '',
      zone_alias        INTEGER             NOT NULL
         REFERENCES zone(id),
      destination       VARCHAR(50)         NOT NULL DEFAULT '',
      zone_destination  INTEGER             NOT NULL
         REFERENCES zone(id),
      recursive         INTEGER             NOT NULL DEFAULT 0,
      PRIMARY KEY (alias,zone_alias,recursive)
   );

   CREATE TABLE ip (
      ip                VARCHAR(16)  UNIQUE NOT NULL DEFAULT '',
      machine           INTEGER             NOT NULL
         REFERENCES machine(id),
      PRIMARY KEY (ip)
   );

   CREATE TABLE zone_machine (
      zone              INTEGER             NOT NULL
         REFERENCES zone(id),
      machine           INTEGER             NOT NULL
         REFERENCES machine(id),
      PRIMARY KEY (zone,machine)
   );

Quant à la table zone, on peut commencer à l'alimenter avec quelques lignes.

   INSERT INTO zone(zone) VALUES ('zone1');
   INSERT INTO zone(zone) VALUES ('zone2');
   INSERT INTO zone(zone) VALUES ('zone3');
   INSERT INTO zone(zone) VALUES ('zone4');

Exemple de script

Un scénario assez simple consiste à afficher une liste de valeurs d'une table.

dns-zone-list

Ce script utilise Class::DBI pour lister les zones contenues dans la table « zone ».

       1 #!/usr/bin/perl
       2 
       3 use strict;
       4 use warnings;
       5 
       6 package DNS::Manager::DBI;
       7 use base 'Class::DBI';
       8 DNS::Manager::DBI->connection('dbi:SQLite:dbname=dns.db', '', '');
       9 
      10 package DNS::Manager::zone;
      11 use base 'DNS::Manager::DBI';
      12 DNS::Manager::zone->table('zone');
      13 DNS::Manager::zone->columns(All => qw/id zone/);
      14 
      15 my @zones = DNS::Manager::zone->retrieve_all;
      16 foreach my $zone (@zones) {
      17     print $zone->zone, "\n";
      18 }

Les lignes 6 à 8 permettent la connexion à la base de données.

Les lignes 10 à 13 permettent la création d'un objet DNS::Manager::zone qui est lié à la table zone de la base de données. Les colonnes à utiliser sont listées à l'aide de la méthode columns. Il existe de nombreuses façons de déclarer des colonnes, comme décrit dans la documentation de CDBI.

Il est aussi possible de le réaliser automatiquement à l'aide de Class::DBI::Loader [2], qui sera abordé plus loin.

Enfin, dans les lignes 15 à 18, on récupère tous les enregistrements de la table et l'on affiche pour chacun d'eux le champ zone.

On peut améliorer la portabilité du code, si on a besoin de changer l'espace de nom par exemple, en utilisant __PACKAGE__ lors des appels de méthodes de classe.

Le script précédent deviendrait alors :

       1 #!/usr/bin/perl
       2 
       3 use strict;
       4 use warnings;
       5 
       6 package DNS::Manager::DBI;
       7 use base 'Class::DBI';
       8 __PACKAGE__->connection('dbi:SQLite:dbname=dns.db', '', '');
       9 
      10 package DNS::Manager::zone;
      11 use base 'DNS::Manager::DBI';
      12 __PACKAGE__->table('zone');
      13 __PACKAGE__->columns(All => qw/id zone/);
      14 
      15 my @zones = __PACKAGE__->retrieve_all;
      16 foreach my $zone (@zones) {
      17     print $zone->zone, "\n";
      18 }

Pour vérifier la syntaxe du script et l'exécuter :

   % perl -c dns-zone-list
   dns-zone syntax OK
   % perl dns-zone-list
   zone1
   zone2
   zone3
   zone4

dns-file-list-iterator

Si le nombre d'enregistrements dans une table est potentiellement élevé, vous auriez intérêt à utiliser des itérateurs. En effet, dans la version précédente, tous les enregistrements sont retournés en bloc, puis affichés un à un.

En utilisant un itérateur, il suffit de demander l'enregistrement suivant à l'aide de la méthode next(). La seule modification incontournable réside dans le type d'objet retourné par la méthode retrieve_all(). Un scalaire est utilisé plutôt qu'un tableau.

Au final, le code est très proche, et la réécriture du script précédent est la suivante :

    #!/usr/bin/perl

    use strict;
    use warnings;

    package DNS::Manager::DBI;
    use base 'Class::DBI';
    __PACKAGE__->connection('dbi:SQLite:dbname=dns.db', '', '');

    package DNS::Manager::zone;
    use base 'DNS::Manager::DBI';
    __PACKAGE__->table('zone');
    __PACKAGE__->columns(All => qw/id zone/);

    my $iterator = __PACKAGE__->retrieve_all;
    while (my $zone = $iterator->next) {
        print $zone->zone, "\n";
    }

dns-zone (une version permettant l'ajout et la suppression)

Cette version du script s'appuie sur quelques principes assez simples.

Tout d'abord, les déclarations du paquetage DNS::Manager::zone et la connexion à la base de données sont réalisées dans un fichier séparé, inclus au début du script. Quelques fonctions sont écrites dans un module séparé pour diverses fonctions élémentaires de vérification.

Ensuite, une table de distribution [6] contient les différentes actions que peut accomplir le script. Pour chacune de ces actions, une référence vers une fonction anonyme est passée.

Grâce au module standard Getopt::Long [5], les arguments de la ligne de commande sont déroulés et une boucle foreach sur les arguments permet de traiter en une ligne toutes les valeurs successives.

       1 #!/usr/bin/perl
       2 
       3 # un commentaire pour Sonia
       4 
       5 use strict;
       6 use warnings;
       7 use Getopt::Long;
       8 
       9 use lib 'lib';
      10 use DNS::Manager;
      11 use DNS::Manager::Utils;
      12 
      13 my %action = (
      14     list => sub {
      15         my $internal = shift;
      16         foreach ( DNS::Manager::Zone->retrieve_all ) {
      17             next if not $_->managed and not $internal;
      18             print $_->zone;
      19             print $_->managed ? '' : '*';
      20             print "\n";
      21         }
      22     },
      23 
      24     add => sub {
      25         my ($zone,$internal) = @_;
      26         if ( exists_zone($zone) ) {
      27             warn "[$0] « $zone » existe déjà\n";
      28         }
      29         else {
      30             print "[$0] ajout de « $zone »\n";
      31             DNS::Manager::Zone->insert({
      32                 zone => $zone,
      33                 managed => ($internal) ? 0 : 1,
      34             });
      35         }
      36     },
      37 
      38     remove => sub {
      39         my $zone = shift;
      40         if ( exists_zone($zone) ) {
      41             print "[$0] suppression de « $zone »\n";
      42             DNS::Manager::Zone->search(zone=>$zone)->delete_all;
      43         }
      44         else {
      45             warn "[$0] « $zone » n'existe pas\n";
      46         }
      47     },
      48 
      49 );
      50 
      51 my %zone;
      52 my $internal;
      53 GetOptions (
      54     'internal'      => \$internal,
      55     'add=s'         => \@{ $zone{add} },
      56     'remove=s'      => \@{ $zone{remove} },
      57     'list'          => sub { $action{list}->() }, 
      58     'full-list'     => sub { $action{list}->(1) },
      59     'help'          => sub { usage($0) },
      60 );
      61 
      62 for my $action (qw/add remove/) {
      63     foreach ( split /,/, join ',', @{ $zone{$action} } ) {
      64         $action{$action}->($_,$internal);
      65     }
      66 }

La fonction exists_zone(), du module DNS::Manager::Utils permet de vérifier qu'une zone existe dans la base. Cette fonction est appelée lors des ajouts et des suppressions.

    sub exists_zone {
        my $zone = shift;
        DNS::Manager::Zone->retrieve(zone=>$zone) or return 0;
        return 1;
    }

La première action décrite concerne l'affichage d'une liste de zones lignes 15 à 23. La méthode retrieve_all() récupère comme son nom l'indique l'ensemble des lignes de la table et retourne une liste d'objets. Il suffit alors d'affecter de parcourir un tableau et d'appeler successivement les méthodes zone() et managed() pour afficher leur contenu.

L'ajout de zone (lignes 25 à 37) utilise une fonction qui effectue simplement un retrieve. L'avantage de cette méthode est qu'elle retourne 0 (assimilable à « faux ») si aucun enregistrement ne correspond à un critère fourni. Une ligne est ajoutée à la base s'il n'existe pas déjà une ligne similaire.

La suppression de zone (lignes 39 à 48) fonctionne sur le même schéma que l'ajout, avec une vérification d'existence au préalable. Comme il est possible d'appeler la méthode delete_all() sur une liste d'objets, et que la méthode search() retourne une liste d'objets, la formulation est très élégante (ligne 43).

La récupération des options (lignes 54 à 61) avec Getopt::Long est un jeu d'enfant et permet l'utilisation d'options courtes. Si la syntaxe peut sembler déroutante, la documentation regorge d'exemples. Les valeurs du hachage sont toutes des références vers des scalaires, des listes, ou des fonctions (anonymes ou non).

La partie vraiment active du script est comprise dans les lignes 63 à 67. Pour chaque zone des listes zone{add} et zone{remove}, la liste est remise à jour si plusieurs zones sont fournies selon différentes syntaxes :

   % perl dns-zone --add zone5,zone6 --add zone7 --remove zone4,zone58
   [dns-zone] ajout de « zone5 »
   [dns-zone] ajout de « zone6 »
   [dns-zone] ajout de « zone7 »
   [dns-zone] suppression de « zone4 »
   [dns-zone] « zone58 » n'existe pas

Ce script couvre les besoins élémentaires de gestion d'un objet DNS::Manager::Zone mais nous n'avons pas pris en compte les descriptions des relations entre objets, et donc des relations entre tables.

Description des relations entre tables

Prenons la relation qui existe entre une « machine » et « ip ». Une machine peut disposer de plusieurs interfaces réseau, et donc avoir plusieurs adresses IP. Il est important de souligner qu'une adresse ne peut appartenir qu'à une machine. On a donc une relation 1-n entre « machine » et « ip ».

Certes, dans des contextes de cluster de haute disponibilité ou dans un souci de répartition de charge, il est possible d'associer des adresses IP virtuelles et d'avoir une adresse du réseau partagée entre plusieurs machines. Néanmoins, le schéma est ainsi constitué, et il permet surtout d'illustrer les relations has_a() et has_many().

En supposant que les déclarations des classes DNS::Manager::Machine et DNS::Manager::IP ont été effectuées, la relation entre les deux classes s'écrit tout simplement :

    DNS::Manager::Machine->has_many(ips => 'DNS::Manager::IP');
    DNS::Manager::IP->has_a(machine => 'DNS::Manager::Machine');

Cette écriture nous permet alors de manipuler des données dans la base de données aisément, sans écrire une ligne de SQL.

   foreach my $machine ( DNS::Manager::Machine->retrieve_all ) {
      print $machine->machine, "\n";
      foreach my $ip ($machine->ips) {
         print ' -> ', $ip->ip, "\n";
      }
   }

Le résultat de l'exécution du code ci-avant serait une liste des machines et pour chacune, une liste de ses adresses IP.

   machine1
    -> 1.1.1.1
    -> 1.1.1.2
   machine2
    -> 1.1.1.3
    -> 1.1.1.4

Quelle que soit la méthode utilisée par CDBI pour effectuer cette opération (sous-requête ou jointure), nous n'avons qu'à nous soucier de la logique de notre script, et utiliser un appel de méthode (ici, la méthode ips de la classe DNS::Manager::Machine) pour obtenir une liste d'objets, que nous pouvons parcourir à l'aide d'une boucle foreach.

Une facilité du même ordre existe pour l'ajout. Imaginons que la machine nommée hephaistos se voit attribuer une nouvelle interface, avec l'adresse IP 192.168.12.13. À l'aide de CDBI,

    my ($machine) = DNS::Manager::Machine->search(machine => 'hephaistos');
    $machine->add_to_ips({ ip => '192.168.12.13' });

La première ligne retourne une liste d'objets, ce qui justifie la présence de parenthèses autour de $machine. En fait, dans notre base de données, nous supposons qu'une seule machine porte ce nom, ce qui nous permet de passer à la ligne suivante sans trop nous poser de questions. La méthode add_to_ips() est appelée sur l'objet $machine. Minute ! D'où sort cette méthode ?

C'est là aussi la magie de CDBI. En décrivant la relation avec has_many() cette méthode a été implicitement créée. Et il faut bien reconnaître que ça facilite la vie en évitant l'écriture de deux ou trois requêtes SQL sur deux tables.

La mise à jour suit la même simplicité d'écriture. Enfin, la suppression est aussi simple, et s'applique en cascade. En réalité, il est possible de modifier le comportement de CDBI par rapport aux suppressions (mise à NULL au lieu de suppression de lignes, par exemple), mais le comportement par défaut est la suppression des lignes liées.

    DNS::Manager::Machine->search(machine => 'hephaistos')->delete_all;

Cette simple ligne permet d'effectuer une recherche dans la table machine pour les machines dont le nom est exactement hephaistos. Compte tenu des contraintes sur la table machine, cette recherche ne peut retourner au plus qu'un objet, mais dans tous les cas, la méthode delete_all() supprimera tous les objets concernés et donc les lignes correspondantes dans la table machine. Là où la magie de CDBI nous saisit encore, c'est que grâce à has_many(), les lignes correspondantes dans la table ip seront également supprimées. On devine aisément la puissance de l'outil et la prudence qu'il doit susciter. En plus, c'est très sexy à lire.

Ce paragraphe a décrit les relations 1-n, mais CDBI peut-il faire encore plus ?

Comment modéliser les relations n-m ?

Si l'on se fie uniquement à la documentation du module, la réponse à cette question est : « On ne peut pas. Si vous le voulez, faites-le vous-même. »

Mais si l'on fouille sur le wiki de CDBI, la réponse est beaucoup moins frustrante. On peut voir une relation n-m comme la nécessité de créer une table d'association, qui peut éventuellement être porteuse d'information.

CDBI permet de décrire cela, avec beaucoup de sucre syntaxique.

   DNS::Manager::Machine->has_many(zone_machines => 'DNS::Manager::ZoneMachine');
   DNS::Manager::ZoneMachine->has_a(machine => 'DNS::Manager::Machine');
   DNS::Manager::Zone->has_many(zones_machine => 'DNS::Manager::ZoneMachine');
   DNS::Manager::ZoneMachine->has_a(zone => 'DNS::Manager::Zone');
   DNS::Manager::Machine->has_many(zones => ['DNS::Manager::ZoneMachine' => 'zone']);
   DNS::Manager::Zone->has_many(machines => ['DNS::Manager::ZoneMachine' => 'machine']);

Dans cette relation, trois tables sont en jeu : machine, zone et la table d'association zone_machine, qui contiendra les clefs étrangères de machine et zone.

Les relations sont décrites assez classiquement par les quatre premières lignes en utilisant has_a() et has_many(). Les deux dernières lignes décrivent la relation n-m. Elles permettent l'ajout ou la suppression dans la table d'association zone_machine lors de l'utilisation de la méthode add_to_zones() sur un objet DNS::Manager::Machine ou de la méthode delete().

   my $machine = 'machine1';
   my @zones = qw/ zone1  zone2  zone3  zone4 /;
   my @ips = qw/ 1.1.1.1  1.1.1.2  1.1.1.3 /;

   my $new_machine = DNS::Manager::Machine->insert({ machine => $machine });

   foreach (@zones) {
      my ($zone_obj) = DNS::Manager::Zone->search({zone=>$_});
      $new_machine->add_to_zones({ zone => $zone_obj->id });
   }

   foreach (@ips) {
      $new_machine->add_to_ips({ ip => $_ });
   }

Dans cet exemple, la troisième ligne ajoute une entrée dans la table machine. Les quatre lignes suivantes parcourent une liste de zones, et pour chacune, obtiennent l'identifiant et ajoutent la zone. Enfin, les adresses IP sont ajoutées.

Comment traiter les schémas complexes ?

En particulier, comment modéliser plusieurs clefs étrangères composites dans une table référençant la même clef primaire d'une autre table ?

Ce cas de figure n'est pas non plus très courant, mais si le schéma de votre base en comporte, voici une astuce tirée du wiki pour les gérer.

Ici encore, on trouve beaucoup de sucre syntaxique.

    DNS::Manager::Zone->has_many(
        zones_alias => ['DNS::Manager::Alias' => 'zone_alias'], 
        'zone_destination'
    );
    DNS::Manager::Zone->has_many(
        zones_destination => ['DNS::Manager::Alias' => 'zone_destination'], 
        'zone_alias'
    );

Il s'agit de faire entendre à CDBI qu'il y a une relation décrite à l'aide de la méthode has_many(), sauf que dans le cas de zones_alias, il y a autre chose, en l'occurrence zone_destination.

Ensuite, le symétrique est décrit (seconde ligne).

Comment rechercher ?

L'information est recherchée pour les cas les plus courants avec la méthode search(). Dans les quelques lignes ci-avant, nous l'avons utilisée pour identifier une ligne pour la suppression ou l'ajout dans des tables référencées.

En fait, il existe une variante, search_like(), qui utilise le prédicat LIKE. On peut alors spécifier des motifs comme en SQL pour les correspondances approchées en utilisant « % » et « _ ».

Comme pour search(), les recherches peuvent concerner plusieurs attributs. En outre, il est possible de spécifier une clause ORDER BY dans une référence de hachage.

   my @machines = DNS::Manager::Machine->search(
                  machine => 'machine1', 
                  visible => 1
   );
   my @zones = DNS::Manager::Zone->search_like(
                  zone => 'z_n%',
                  managed => 1,
                  { order_by=>'id DESC, zone' }
   );

Si ces fonctions sont intéressantes et pratiques, elles restent limitées parce qu'elles ne s'appliquent qu'à une classe à la fois et donc à une seule table.

Nous verrons un peu plus loin comment bâtir des recherches qui ressemblent à des vraies requêtes ensemblistes, grâce au module Class::DBI::AbstractSearch [3].

Quelques modules supplémentaires pour se faciliter la vie

Détecter la structure d'une base avec Class::DBI::Loader

CDBI est un bon moyen pour fournir une couche d'abstraction qui étend les possibilités du SGBD sous-jacent.

L'abstraction reste appréciable si le SGBD dispose des fonctionnalités exigées (pour le meilleur et pour le pire) comme les déclencheurs, transactions, etc.

Mais l'on a vu que pour chaque table, un objet lui est associé. Dans le cas le plus simple, quatre lignes de description de l'objet sont nécessaires. Nous n'avons pas de clefs composites et nous n'avons pas abordé les relations.

Si notre schéma comporte moins d'une dizaine de tables et que l'on souhaite gérer finement le schéma, une description intégrale reste possible. Si le schéma comprend quelques dizaines (voire centaines) de tables, avec un grand nombre de colonnes, on frise la crise de nerf.

C'est pour cela que le module Class::DBI::Loader [2] existe. Il peut générer à partir du dictionnaire du SGBD (pour ceux qui sont supportés, à savoir à l'heure de la rédaction de cet article, MySQL, PostgreSQL, SQLite, Informix, DB2, Oracle, Sybase) tous les objets CDBI et les relations. Pour l'anecdote, il existe un Class::DBI::Loader::GraphViz.

    use Class::DBI::Loader;
    my $loader = Class::DBI::Loader->new(
        dsn       => 'dbi:SQLite:dbname=dns.db',
        namespace => 'DNS::Manager',
    );

La différence la plus flagrante (outre l'absence de description très lourde) est qu'il suffit de fournir un nom pour la classe racine et tous les objets déductibles seront déduits et créés dans l'espace de nommage.

Le code s'en trouve considérablement allégé, mais au prix des deux conditions incontournables :

  1. Le SGBD doit être supporté par Class::DBI::Loader.

  2. Le schéma doit être correctement décrit dans le dictionnaire du SGBD. Un usage rigoureux des contraintes PRIMARY, CHECK, et autres REFERENCES est indispensable.

Ce module escamote la partie fastidieuse. Nous allons voir un autre aspect très appréciable, relatif aux recherches complexes (qui dépassent le cadre des recherches sur critères simples).

Rechercher un peu plus finement avec Class::DBI::AbstractSearch

Autant l'avouer tout de suite, Class::DBI::AbstractSearch [3] ne permet pas non plus de retourner des données provenant de plusieurs tables. Pour cela, SQL reste indispensable.

Par contre, il est souvent nécessaire de retourner un sous-ensemble de résultats d'une requête. Pour certains SGBD, on peut utiliser une clause LIMIT. Mais tous les SGBD ne sont pas égaux en fonctionnalités, et les syntaxes ne sont pas toutes identiques. CDBI::AbstractSearch greffe SQL::Abstract::Limit dans CDBI. Il est possible de ne retourner qu'une dizaine de lignes pour une pagination de résultats dans une application.

   use Class::DBI::AbstractSearch;

   my @machines = DNS::Manager::Machine->search_where(
                     machine => [ 'hephaistos', 'odin' ],
                     visible => { '!=', 0 },
                  );

   my @machines = DNS::Manager::Machine->search_where(
                     {
                        machine => [ '%o%' ],
                        visible => { '!=', 0 },
                     },
                     {
                        logic         => 'AND',
                        order_by      => "machine DESC",
                        limit_dialect => 'LimitOffset',
                        limit         => 10,
                        offset        => 20,
                     },
                  );

Utiliser du SQL tout de même ? C'est possible !

Supposons que l'on souhaite identifier 3 zones qui contiennent moins de 5 enregistrements A. Dans notre schéma, cela correspond aux zones qui ont moins de 5 machines.

Avec des fonctions de regroupement et des restrictions, il est nécessaire d'écrire une requête SQL personnalisée, d'autant que la requête SQL concerne deux tables.

   DNS::Manager::Zone->columns(TEMP => qw/nb_machines/);
   DNS::Manager::Zone->set_sql(small_zones => q{
       SELECT zone.id, COUNT(zone_machine.machine) AS nb_machines
       FROM zone, zone_machine
       WHERE zone.id = zone_machine.zone
       GROUP BY zone.id
       HAVING nb_machines <= 5
       LIMIT 3
   });
     
   foreach my $zone (DNS::Manager::Zone->search_small_zones) {
       printf "%s : %d machine(s)\n",
           $zone->zone, scalar $zone->nb_machines;
   }

La première ligne précise que la colonne nb_machines est non-persistente. Grâce à cette astuce, il sera possible d'utiliser les accesseurs classiques de CDBI sur cette colonne bien qu'elle n'existe pas dans la base.

Dans le cas contraire, il aurait fallu utiliser les méthodes au niveau de DBI pour manipuler les données retournées.

À l'exécution, on aurait une sortie comme :

   % perl dns-zone-list-small-zones
   zone1 : 5 machine(s)
   zone2 : 1 machine(s)
   zone3 : 1 machine(s)

Si vous préférez utiliser les connexions DBI et/ou que vous ne souhaitez pas déclarer des colonnes temporaires, c'est bien entendu possible. C'est un peu moins sexy et cela oblige à modifier la requête SQL pour obtenir le même résultat. En effet, alors qu'avec CDBI, nous pouvions retrouver le nom de la zone à partir de sa clef primaire, nous sommes désormais contraints de faire apparaître le nom de la zone dans les colonnes à retourner.

   DNS::Manager::DBI->set_sql(small_zones => q{
       SELECT zone.zone, COUNT(zone_machine.machine) AS nb_machines
       FROM zone, zone_machine
       WHERE zone.id = zone_machine.zone
       GROUP BY zone.id
       HAVING nb_machines <= 5
       LIMIT 3
   });

   my $sth = DNS::Manager::DBI->sql_small_zones;
   $sth->execute();

   while (my $row = $sth->fetch) {
       my $zone = $row->[0];
       my $nb_machines = $row->[1];
       printf "%20s : %d machines\n",
           $zone, scalar $nb_machines;
   }

On peut aussi utiliser le mécanisme des placeholders (ou paramètres fictifs), qui permet de préparer une requête et de substituer des valeurs après une phase de précompilation. Dans ce cas, la requête SQL décrite à l'aide de set_sql() contient des ? à la place des valeurs à substituer et la méthode appelée prendra en paramètre une liste de valeurs. Seules les valeurs sont remplaçables, pas les noms de tables ou de colonnes. Je vous renvoie à la documentation de DBI et CDBI pour plus d'exemples.

Des déclencheurs (triggers) et des contraintes

Un déclencheur est un ensemble d'opérations réalisées à la survenue d'un événement. Ces événements sont très couramment des modifications (ajout, mise à jour, suppression) et les opérations sont réalisées au choix avant ou après l'événement en question. La documentation de CDBI révèle des informations précieuses sur la survenue des événements (par exemple, SELECT).

Sachez que l'on peut écrire plusieurs déclencheurs pour le même événement, mais qu'il n'est pas possible de spécifier leur ordre d'exécution.

Par exemple, pour ajouter une machine d'office dans une zone par défaut, on peut écrire un déclencheur comme :

   DNS::Manager::Machine->add_trigger(after_create  => \&add_to_zone1);

   sub add_to_zone1 {
      my ($self,%args) = @_;
      $self->add_to_zones({ zone => 1 });
   }

Ce qui peut aussi s'écrire :

   DNS::Manager::Machine->add_trigger(after_create  => sub {
      my ($self,%args) = @_;
      $self->add_to_zones({ zone => 1 });
   });

Dans la même veine, les contraintes procèdent de la même manière. Pour une classe donnée, une méthode add_constraint() prend en paramètre un nom de contrainte et une paire « nom de colonne / coderef ».

Si vous trouvez la gestion des contraintes un peu lourde, il existe un petit raccourci pour des contraintes simples de domaine ou de valeur à l'aide de la méthode constraint_column().

Support des transactions

Les transactions ne sont pas en reste. Si vous ne souhaitez pas les implémenter par le biais de votre SGBD ou qu'elles ne sont pas disponibles, vous pouvez toujours en utiliser à travers CDBI. En fait, les mécanismes employés sont ceux de DBI et les méthodes dbi_commit() et dbi_rollback() sont des alias vers celles de DBI.

La documentation présente un enrobage agréable à partir d'une suggestion de Dominic Mitchell. L'astuce consiste à désactiver la validation automatique (autocommit) localement, et de valider toute la transaction en fin de bloc. Ceci se fait naturellement parce qu'en sortie de bloc, la valeur d'autocommit est restaurée et qu'un commit est exécuté. Mais dans le cas où l'exécution du code provoque une erreur, alors la méthode dbi_rollback() est appelée.

    sub do_transaction {
       my $class = shift;
       my ( $code ) = @_;
       local $class->db_Main->{ AutoCommit };

       eval { $code->() };
       if ( $@ ) {
          my $commit_error = $@;
          eval { $class->dbi_rollback };
          die $commit_error;
       }
    }

    DNS::Manager::DBI->do_transaction( sub {
       my $zone = DNS::Manager::Zone->insert(zone => 'zone58');
       my $machine = DNS::Manager::Machine->insert({ machine => 'machine58' });
       $machine->add_to_ips({ ip => '10.1.1.58' });
       $machine->add_to_zones({ zone => $zone->id });
    } );

La transaction porte sur l'ajout d'une zone, d'une machine, et pour cette machine, l'ajout d'une adresse et le rattachement à la zone nouvellement créée.

Application pratique : génération de fichiers de zone pour le DNS

À partir des données dont nous disposons dans notre base de données, la fabrication de fichiers de zone (correspondances directes et inverses) est relativement aisée.

Il existe quelques modules sur le CPAN pour faciliter l'écriture des entêtes de fichiers de zone et leur contenu. Comme le format des données est relativement trivial, le code ci-après prend en charge directement l'écriture des enregistrements. Bien que le résultat soit fonctionnel, vous préférerez peut-être utiliser des modules existants pour remplir ces fonctions.

Correspondances inverses

Pour les correspondances inverses, on met en regard une adresse réseau (IP) et un nom complet (FQDN). Pour ce type de requêtes DNS, les alias ne sont d'aucune utilité. Il nous faut obtenir la liste des machines et de leur adresse IP pour une zone donnée.

La requête SQL qui fournit ces informations est la suivante :

   SELECT DISTINCT machine.machine AS src, ip.ip AS dst
   FROM  zone, zone_machine, machine, ip
   WHERE zone.zone = '$zone'
     AND zone.id = zone_machine.zone
     AND zone_machine.machine = machine.id
     AND machine.id = ip.machine
     AND machine.visible = 1

Correspondances directes

Pour les correspondances directes, la requête DNS porte sur un nom de domaine complet (FQDN) et en réponse, le DNS fournit une adresse réseau avec si nécessaire les étapes de résolution des CNAME.

On utilise les données de la requête SQL qui retourne la liste des machines et leur adresse à laquelle on ajoute les alias qui pointent vers la même zone et ceux qui pointent vers une autre zone. La requête qui ajoute ces informations est la suivante :

   SELECT CASE
         WHEN recursive = 1
         THEN '*.' || alias
         ELSE alias
      END AS src,
      destination AS dst 
   FROM alias, zone z1, zone z2
   WHERE z1.zone = '$zone'
     AND alias.zone_alias = z1.id
     AND z2.zone = '$zone'
     AND alias.zone_destination = z2.id
   UNION
   SELECT CASE
         WHEN recursive = 1
         THEN '*.' || alias
         ELSE alias
      END AS src,
      destination || '.' || z2.zone || '.' AS dst
   FROM alias, zone z1, zone z2
   WHERE z1.zone = '$zone'
     AND alias.zone_alias = z1.id
     AND z2.zone <> '$zone'
     AND alias.zone_destination = z2.id

Elle est ajoutée à la précédente par un opérateur UNION de SQL.

En pratique, on peut supposer que les deux SELECT pourraient être regroupés en un seul, et que le serveur DNS optimiserait lors des transferts de zones ce qui peut l'être, mais pour la beauté du geste, je laisse ces concaténations sauvages. Ce code a été testé sur PostgreSQL, SQLite et Oracle.

Pour bénéficier de la souplesse de CDBI, nous allons préciser que les deux colonnes que nous manipulons dans ces requêtes (src et dst) n'ont pas d'existence réelle, et donc qu'elles ne doivent pas être prises en compte dans les mécanismes de persistance.

Ensuite, nous ajoutons une requête SQL à l'aide de la méthode set_sql().

L'utilisation du résultat sera identique à ce que nous avons vu jusqu'ici.

   DNS::Manager::Alias->columns(TEMP => qw/src dst/);
   DNS::Manager::Alias->set_sql(for_dump => $dump_query);

   my @aliases = DNS::Manager::Alias->search_for_dump;

Pour ne pas empiéter sur les colonnes réelles, il est nécessaire de nommer les colonnes retournées par la requête avec des noms différents. Sans cette précaution, aucune modification ne sera enregistrée dans la base lors des insertions ou mises à jour pour les colonnes qui apparaissent à la fois dans All et TEMP.

Comme il est possible de définir des requêtes SQL à tout moment, avec les méthodes associées, ce script peut construire une requête et la soumettre à CDBI en cours d'exécution, ce qui est assez spectaculaire en soi.

La version intégrale du script de génération des zones pourrait ressembler à ceci :

   #!/usr/bin/perl

   use strict;
   use warnings;

   use Getopt::Long;
   use Regexp::Common;
   use Fcntl ':flock';
   use Socket;

   use lib 'lib';
   use DNS::Manager;
   use DNS::Manager::Utils;

   my ($direct, $reverse, $output_file, $zone);
   GetOptions (
    'direct'      => \$direct,
    'reverse'     => \$reverse,
    'output-file' => \$output_file,
    'zone=s'      => \$zone,
    'help'        => sub { usage($0); exit 1 },
   );

   die "[$0] quelle zone ?\n" unless $zone;
   die "[$0] zone non gérée\n" unless is_managed_zone($zone);
   die "[$0] ni direct ni inverse ???\n" if !$reverse and !$direct;

   my $zone_filename = '';

   my $query = make_alias_dump_query($zone, $direct);

   DNS::Manager::Alias->set_sql(for_dump => $query);

   my $result = '';
   foreach my $alias (DNS::Manager::Alias->search_for_dump) {
     if ($direct) {
         $result .= $alias->src . ' IN ';
         $result .=
            $alias->dst =~ /$RE{net}{IPv4}/ && inet_aton($alias->dst) ?
                'A ' . $alias->dst :
            'CNAME ' . $alias->dst ;
         $result .= "\n";
      }
      elsif ($reverse) {
         my $reverse_ip = join '.', (reverse(split /\./, $alias->dst));
         $result .= sprintf("%s.in-addr.arpa. IN PTR %s.%s.\n",
                      $reverse_ip, $alias->src, $zone
                    );
      }
   }

   $zone_filename =
      $reverse ? 'inverse.' . $zone :
      $direct  ?  'direct.' . $zone :
                     '___.' . $zone;
     
   open(my $lf, ">$zone_filename.lock")
      or die "Impossible d'ouvrir un fichier verrou : $!\n";
   flock($lf, 2)
      or die "Impossible de verrouiller : $!\n";

   my $serial = compute_serial($zone_filename);
   $result = generate_header($serial, $zone) . $result ;

   if ($output_file) {
      open(my $of, "> $zone_filename.$serial")
         or die "Impossible d'ouvrir le fichier $zone_filename.$serial : $!\n";
      print $of $result
         or die "Impossible d'écrire dans le fichier $zone_filename.$serial : $!\n";
      close($of)
         or warn "Impossible de fermer dans le fichier $zone_filename.$serial : $!\n";
         
      if (-f $zone_filename) {
        unlink $zone_filename
          or die "Impossible de supprimer le lien $zone_filename\n";
      }
      symlink "$zone_filename.$serial", $zone_filename
         or die "Impossible de créer le lien $zone_filename\n";
   } else {
      print $result ;
   }

   END {
      if ($zone_filename && -f "$zone_filename.lock") {
         unlink "$zone_filename.lock"
      }
   }

Quelques fonctions non décrites ici sont appelées pour générer un numéro de série valide, comportant la date du jour et un incrément sur deux chiffres ainsi qu'un entête de fichier de zone valide. Ces fonctions sont très largement inspirées de Perl for System Administrator [7].

La requête SQL est également générée à l'aide d'une fonction qui retranscrit à la virgule près ce qui a été décrit un peu plus tôt.

DNS::Manager::Utils exporte les quelques fonctions utilisées dans le script précédent, à savoir usage(), is_managed_zone(), make_alias_dump_query, compute_serial() et generate_header. Le code de ce module utilitaire ainsi que les scripts sont consultables en ligne [10].

Au final, un fichier produit aurait cet aspect :

   % perl dns-file --zone zone1 --direct ~/src/dnsmam
   ; Generated by dns-file. Arguments were: "--zone zone1 --direct"
   ; Don't edit manually, it will be overwritten.
   ; This program was run by bifo (uid=756) at Sat Aug 19 19:04:35 2006.
   $TTL 1d
   @ IN SOA ns0.zone1. nsmaster.zone1. (
                                   2006081900             ; serial
                                        3h             ; refresh
                                        1h             ; retry
                                        1w             ; expire
                                        1h)            ; TTL
   ;
                             IN NS ns0.zone1.
                             IN NS ns1.zone1.
   ;
   machine1  IN A 10.1.1.1
   machine2  IN A 10.1.1.2
   machine3  IN A 10.1.1.3
   machine4  IN A 10.1.1.4
   machine5  IN A 10.1.1.5
   machine6  IN A 10.1.1.6
   externe   IN CNAME autre.domaine.
   *.externe-recursif IN CNAME autre.domaine.
   service1  IN CNAME machine1
   *.service1 IN CNAME service1

Les lignes introduites par « ; » sont des commentaires et sont ignorées.

Pour aller plus loin

Il serait aussi possible d'utiliser les informations relatives aux projets pour supporter les champs SRV. En partant de ce principe, il serait possible d'étendre aux autres types de champs comme les enregistrements LOC et les clefs de machines KEY ainsi que les signatures (enregistrements SIG), si le DNS fonctionne en DNSSEC.

Une autre amélioration dans la factorisation serait souhaitable, mais cette réécriture se traduirait peut-être par du code un peu moins lisible. Chaque script est conçu sur le même modèle (à l'exclusion du générateur de fichiers de zone), en exploitant une table de distribution, Getopt::Long et bien entendu CDBI.

L'utilisation d'un module pour les écritures dans les fichiers de zone constitue aussi une évolution souhaitable. Il en existe un certain nombre, comme DNS::Manager::ZoneParse, Net::DNS::Manager::Zone::Parser, Net::DNS::Manager::ZoneFile, ou encore DNS::Manager::ZoneFile. Consultez le CPAN pour plus d'informations.

Enfin, le versionnement des fichiers de zone après écriture devrait être pris en compte, et si possible à l'aide d'un module comme SVK [8] plutôt qu'une conservation intégrale de toutes les versions.

Vérification des données produites

La distribution de BIND est assortie d'outils de vérification de fichiers de configuration et de fichiers de zones. Néanmoins, cette opération est fastidieuse lorsque l'on héberge de nombreuses zones.

En effet, named-checkzone permet de vérifier une zone à la fois. Or le fichier de configuration énumère l'ensemble de ces zones avec l'emplacement du fichier de zone.

Voici un script qui permet de vérifier la configuration, d'une part, et toutes les zones qui y sont énumérées. Il est assez peu portable, en ce sens qu'il s'appuie sur des emplacements de configuration que l'on peut trouver sur des distributions basées sur Red Hat. Néanmoins, la logique propre à la vérification en cascade reste la même.

  #!/usr/bin/perl
  # Suggested (brilliantly) by William HOFFMANN

  use strict;
  use warnings;
  use File::Slurp;

  our $conf_file          = '/etc/named.conf';
  our $sysconf_file       = '/etc/sysconfig/named';
  our $check_conf_command = '/usr/sbin/named-checkconf';
  our $check_zone_command = '/usr/sbin/named-checkzone';

  my $sysconf_text = read_file($sysconf_file)
    or die "Could not read '$sysconf_file': $!";

  # Find the chroot's path in /etc/sysconfig
  # Another way to do it would be to source the file and check the var in @ENV
  my ($root_dir) = $sysconf_text =~ m/^ROOTDIR=['"]?([^'"]*)['"]?$/m;
  $root_dir = '' unless $root_dir;

  chomp $root_dir;
  print 'Found a root directory: ', $root_dir, "\n";

  # Get the named.conf path and check it with named_checkconf
  if ( -r "$root_dir/$conf_file" ) { 
      $conf_file = "$root_dir/$conf_file";
  }
  else {
      die "$root_dir/$conf_file is not readable: $!";
  }

  # Variants for named-checkconf
  #   * -t root_dir
  #   * path/to/named.conf
  my @check_command = qq/$check_conf_command $conf_file/;
  run( \@check_command, $conf_file )
      or die "Something went wrong during config file checking: $!\n";

  # If the conf is OK, get the zone list
  my $conf_text = read_file($conf_file);

  # get optionaly the directory in which the zonefiles are stored
  # e.g.  directory "/var/named";
  ( my $zone_basedir ) = $conf_text =~ m/^[^#]*directory\s+"([^"]+)"/;
  print "Found a root for zonefiles: '$zone_basedir'\n"
      unless $zone_basedir =~ /^$/;

  # For each zone,
  #    * get the filename for its datafile
  #    * check the zone file with named_checkzone
  my @zonenames = $conf_text =~ m/zone\s+"([^"]+)"/xmsg;

  my @zones = 
    $conf_text =~
  m/(zone\s+"([^"]+)"\s*IN\s*\{(?:[^}]*|.*file\s*"[^"]+"\s*;.*?\1)\};)/xmsg;

  my %file;

  foreach my $index ( 0..$#zones-1 ) {
      next if $index%2;
      my $zonename = $zones[ $index + 1 ];
      my ($zonefile) = $zones[ $index ] =~ m/^.*file\s*"([^"]+)".*$/gmsx;
      $file{$zonename} = $zonefile;
  }

  foreach my $zone (sort keys %file) {
      print 'zone: ', $zone, ' file: ', $file{$zone}, "\n";
      my $zone_filename = "$root_dir/$zone_basedir/$file{$zone}";
      my @check_command = qq/$check_zone_command $zone $zone_filename/;
      run( \@check_command, $file{$zone} );
  }

  sub run {
      my ( $commandref, $tested ) = @_;
      my @command = @$commandref;
      system(@command);

      if ( $? == -1 ) {
          print "Failed to execute: $!\n";
          return 0;
      }
      elsif ( $? & 127 ) {
          printf "Child died with signal %d, %s coredump\n", ( $? & 127 ),
            ( $? & 128 ) ? 'with' : 'without';
          return 0;
      }
      else {
          my $return = $? >> 8;
          if ( $return == 0 ) {
              print "'$conf_file' seems OK.\n";
          }
          else {
              warn "'@command' failed with code: $?";
              return 0;
          }
      }
      return 1;
  }

La fonction run, pale copie de ce que l'on peut trouver dans perldoc -f system est appelée pour toutes les vérifications à l'aide de named-checkconf et named_checkzone.

Le principe est d'extraire l'emplacement des fichiers depuis les différents fichiers de configuration. Ensuite, le fichier de configuration est testé, et si cette opération se déroule correctement, les noms des zones et les emplacements des fichiers correspondants sont également extraits. Ces informations sont utilisées lors de l'appel de named-checkzone.

Une tronçonneuse suisse ?
Une tronçonneuse suisse ?

Conclusion

Pour un administrateur système, si Perl s'impose comme tronçonneuse suisse incontournable, l'utilisation de bases de données peut tourner au cauchemar.

Grâce à Class::DBI, et un SGBD de son choix, les tâches récurrentes d'interrogation ou de modification peuvent être réalisées très simplement.

Les opérations plus complexes, qui nécessitent l'écriture de requêtes SQL personnalisées, restent possibles. Les fonctionnalités absentes peuvent être ajoutées au niveau applicatif, pour permettre un support des transactions, des contraintes, etc.

À propos de l'auteur

Laurent Gautrot est utilisateur et prosélyte de logiciels libres depuis une dizaine d'années.

Merci aux Mongueurs de toute la Francophonie qui ont assuré la relecture de cet article.

Références

[10] dnsmam - http://l.gautrot.free.fr/dnsmam/

[IE7, par Dean Edwards] [Validation du HTML] [Validation du CSS]