De RackTables à rack

Sébastien Aperghis-Tramoni, sebastien@aperghis.net

But

  • retour d'expérience

  • utilisation de Moose et DBIx::Class

  • projet d'administration système

Contexte

  • mission de 3 mois chez eStat Médiamétrie

  • développement d'un outil de génération de configurations

  • source : RackTables

  • cibles : PXE, Kickstarter, Cfengine, Nagios, Cacti, Bacula, HP iLO, APC RackPDU, switch Cisco

RackTables

  • logiciel de CMDB orienté datacenter

  • PHP/MySQL

  • pas au niveau de GLPI

  • interface claire

  • très adapté aux besoins IT

Choix techniques

  • projet neuf, sans historique

  • notions d'objets avec propriétés transverses

  • pas de contrainte de performances

  • => Moose

  • schéma de la base assez connecté

  • => DBIx::Class

Prototype

  • DBIx::Class::Schema::Loader

  •   dbicdump -o dump_directory=lib -o preserve_case=1 RackTables \
          dbi:mysql:racktables rackuser rackpass
  • génération dans lib/ d'une hiérarchie de modules immédiatement utilisable

Prototype

  •   use strict;
      use Socket;
      use YAML;
      use lib "lib";
      use RackTables;
    
    
      my $name = shift;
    
      my $schema = RackTables->connection("dbi:mysql:racktables",
          "rackuser", "rackpass");
    
      my $obj_rs = $schema->resultset("RackObject")->search(
          { "me.name" => $name },
          { prefetch => { attribute_values => { attr => "attr" } } },
      );

Prototype

  •   while (my $obj = $obj_rs->next) {
          # store object properties
          my %prop;
          $prop{$_} = $obj->$_ for qw< id name asset_no has_problems comment >;
    
          # resolve object type
          my $type_rs = $schema->resultset("Dictionary")->search(
              { chapter_id => 1, dict_key => $obj->objtype_id },
          );
          $prop{type} = $type_rs->first->dict_value;
    
          # find IPv4 addresses
          my $ipv4_rs = $schema->resultset("Ipv4Allocation")->search(
              { object_id => $prop{id} },
          );
          my @addrs = map +{
              type => $_->type,  iface => $_->name,
              addr => inet_ntoa(pack "N", $_->ip),
          }, $ipv4_rs->all;
    
          print YAML::Dump{ properties => \%prop, addresses => \@addrs };
      }

Observations

  • noms de méthodes créés par DBIx::Class::Schema::Loader pas toujours évidents

  • comportement difficile à contrôler

Objets

  • Moose

  • encore mieux que ce qu'on en dit

Simplicité

  • RackMan::File - définition

      package RackMan::File;
    
      use File::Basename;
      use File::Path;
      use Moose;
      use Path::Class;
      use RackMan;
      use namespace::autoclean;
    
      has name    => ( is => "rw", isa => "Str" );
      has path    => ( is => "rw", isa => "Str" );
      has content => ( is => "rw", isa => "Str" );
    
      sub fullpath { ... }
      sub add_content { ... }
      sub read { ... }
      sub write { ... }
    
      __PACKAGE__->meta->make_immutable

Simplicité

  • RackMan::File - utilisation

Délégation

  • RackMan::Config

      package RackMan::Config;
    
      use Config::IniFiles;
      use File::Spec::Functions;
      use Moose;
      use RackMan;
      use namespace::autoclean;
    
      has _config => (
          is => "ro",
          isa => "Config::IniFiles",
          handles => {
              exists  => "exists",
              setval  => "setval",
              newval  => "newval",
              delval  => "delval",
              ...
          }
      );

Pré-traitement

  • RackMan::Config

      sub BUILDARGS {
          my $class = shift;
          my %params;
    
          # ...
    
          my $config = Config::IniFiles->new(%params)
              or RackMan->error("can't parse config file '$params{'-file'}': ",
              @Config::IniFiles::errors);
    
          # ...
    
          return { _config => $config }
      }

Rôles

  • composition dynamique de rôles

  • spécialisation progressive d'un objet

  •   Device -> PDU    -> APC_RackPDU
             -> Server -> HP_ProLiant
             -> Switch -> Cisco_Catalyst

Rôles

  • RackMan::Device

      sub BUILD {
          my ($self, $args) = @_;
    
          my %implemented = map { $_ => 1 } RackMan::Types->implemented;
    
          if ($implemented{$self->object_type}) {
              # determine the type of device and apply the corresponding 
              # role to the object
              my $type_role = __PACKAGE__."::".$self->object_type;
              eval { Class::MOP::load_class($type_role) }
                  or RackMan->error("can't load $type_role: $@");
              $type_role->meta->apply($self);
    
              # "specialise" the object (apply a specialised role) to the object
              # so it knows how to speak with the actual hardware, if needed
              $self->specialise;
          }
      }

Rôles

  • RackMan::Device::{PDU,Server,Switch}

      use constant HW_ROLES => ( APC_RackPDU => qr/^APC\s+AP\d+/ );
      use constant CONFIG_FORMATS => qw< Cacti  Nagios >;
      has class => ( is => "rw", isa => "Str" );
    
      sub specialise {
          my $self = shift;
          my %hw_roles = $self->HW_ROLES; # fetch the sub-roles patterns
    
          # determine the role corresponding to the hardware
          my $hw_type = $self->attributes->{"HW type"};
          return unless $hw_type;
          my ($hw_role) = grep { $hw_type =~ $hw_roles{$_} } keys %hw_roles;
    
          if ($hw_role) {
              # load and apply the role to the object
              $hw_role = __PACKAGE__."::$hw_role";
              $self->class($hw_role);
              eval { Class::MOP::load_class($hw_role) }
                  or RackMan->error("can't load $hw_role: $@");
              $hw_role->meta->apply($self);
          }
      }

Programme final

  • commande rack

  • documentation

  • tests

rack

  • informations :

      rack info samus.example.com
  • différences entre les configs réelles et attendues :

      rack diff samus.example.com
  • génère et écrit les fichiers de configuration :

      rack write samus.example.com
  • envoie la configuration :

      rack push samus.example.com

Questions ?

Merci