453 lines
12 KiB
Perl
453 lines
12 KiB
Perl
#
|
|
# (c) Jan Gehring <jan.gehring@gmail.com>
|
|
# (c) Zane C. Bowers-Hadley
|
|
|
|
package Rex::CMDB::TOML;
|
|
|
|
use 5.010001;
|
|
use strict;
|
|
use warnings;
|
|
|
|
our $VERSION = '0.0.1'; # VERSION
|
|
|
|
use base qw(Rex::CMDB::Base);
|
|
|
|
use Rex::Commands -no => [qw/get/];
|
|
use Rex::Logger;
|
|
use TOML qw(from_toml);
|
|
use Data::Dumper;
|
|
use Hash::Merge qw/merge/;
|
|
|
|
require Rex::Commands::File;
|
|
|
|
sub new {
|
|
my $that = shift;
|
|
my $proto = ref($that) || $that;
|
|
my $self = {@_};
|
|
|
|
$self->{merger} = Hash::Merge->new();
|
|
|
|
if ( !defined $self->{merge_behavior} ) {
|
|
$self->{merger}->specify_behavior(
|
|
{
|
|
SCALAR => {
|
|
SCALAR => sub { $_[0] },
|
|
ARRAY => sub { $_[0] },
|
|
HASH => sub { $_[0] },
|
|
},
|
|
ARRAY => {
|
|
SCALAR => sub { $_[0] },
|
|
ARRAY => sub { $_[0] },
|
|
HASH => sub { $_[0] },
|
|
},
|
|
HASH => {
|
|
SCALAR => sub { $_[0] },
|
|
ARRAY => sub { $_[0] },
|
|
HASH => sub { Hash::Merge::_merge_hashes( $_[0], $_[1] ) },
|
|
},
|
|
},
|
|
'REX_DEFAULT',
|
|
); # first found value always wins
|
|
|
|
$self->{merger}->set_behavior('REX_DEFAULT');
|
|
}
|
|
else {
|
|
if ( ref $self->{merge_behavior} eq 'HASH' ) {
|
|
$self->{merger}->specify_behavior( $self->{merge_behavior}, 'USER_DEFINED' );
|
|
$self->{merger}->set_behavior('USER_DEFINED');
|
|
}
|
|
else {
|
|
$self->{merger}->set_behavior( $self->{merge_behavior} );
|
|
}
|
|
}
|
|
|
|
bless( $self, $proto );
|
|
|
|
# turn roles off by default
|
|
if ( !defined( $self->{use_roles} ) ) {
|
|
$self->{use_roles} = 0;
|
|
}
|
|
|
|
# set the default role path ro 'cmdb/roles'
|
|
if ( !defined( $self->{roles_path} ) ) {
|
|
$self->{roles_path} = 'cmdb/roles';
|
|
}
|
|
|
|
# if parsing failure should be fatal
|
|
# default true
|
|
if ( !defined( $self->{parse_error_fatal} ) ) {
|
|
$self->{parse_error_fatal} = 1;
|
|
}
|
|
|
|
# die if the role does not exist
|
|
# default true
|
|
if ( !defined( $self->{missing_role_fatal} ) ) {
|
|
$self->{missing_role_fatal} = 1;
|
|
}
|
|
|
|
# default to false, config overwrites role settings
|
|
if ( !defined( $self->{roles_merge_after} ) ) {
|
|
$self->{roles_merge_after} = 0;
|
|
}
|
|
|
|
return $self;
|
|
}
|
|
|
|
sub get {
|
|
my ( $self, $item, $server ) = @_;
|
|
|
|
$server = $self->__get_hostname_for($server);
|
|
|
|
my $result = {};
|
|
|
|
# keep this out here so generated when the files are loaded
|
|
# keep it around later for role processing
|
|
my %template_vars;
|
|
|
|
if ( $self->__cache->valid( $self->__cache_key() ) ) {
|
|
$result = $self->__cache->get( $self->__cache_key() );
|
|
}
|
|
else {
|
|
|
|
my @files = $self->_get_cmdb_files( $item, $server );
|
|
|
|
Rex::Logger::debug( Dumper( \@files ) );
|
|
|
|
# configuration variables
|
|
my $config_values = Rex::Config->get_all;
|
|
for my $key ( keys %{$config_values} ) {
|
|
if ( !exists $template_vars{$key} ) {
|
|
$template_vars{$key} = $config_values->{$key};
|
|
}
|
|
}
|
|
$template_vars{environment} = Rex::Commands::environment();
|
|
|
|
for my $file (@files) {
|
|
Rex::Logger::debug("CMDB - Opening $file");
|
|
if ( -f $file ) {
|
|
|
|
my $content = eval { local ( @ARGV, $/ ) = ($file); <>; };
|
|
my $t = Rex::Config->get_template_function();
|
|
$content .= "\n"; # for safety
|
|
$content = $t->( $content, \%template_vars );
|
|
|
|
my ( $ref, $parse_error ) = from_toml($content);
|
|
|
|
# only merge it if we have a actual result
|
|
if ( defined($ref) ) {
|
|
$result = $self->{merger}->merge( $result, $ref );
|
|
}
|
|
else {
|
|
my $error = 'Failed to parse TOML config file "' . $file . '" with error... ' . $parse_error;
|
|
if ( $self->{parse_error_fatal} ) {
|
|
die($error);
|
|
}
|
|
else {
|
|
warn($error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# if use_roles is true, process the roles variablesif set
|
|
# the item has roles and that the roles is a array
|
|
if ( $self->{use_roles}
|
|
&& ( defined( $result->{roles} ) )
|
|
&& ( ref( $result->{roles} ) eq 'ARRAY' ) )
|
|
{
|
|
Rex::Logger::debug("CMDB - Starting role processing");
|
|
|
|
# load each role
|
|
foreach my $role ( @{ $result->{roles} } ) {
|
|
Rex::Logger::debug( "CMDB - Processing role '" . $role . "'" );
|
|
my $role_file = File::Spec->join( $self->{roles_path}, $role . '.toml' );
|
|
|
|
# if the file exists, load it
|
|
if ( -f $role_file ) {
|
|
my $content = eval { local ( @ARGV, $/ ) = ($role_file); <>; };
|
|
my $t = Rex::Config->get_template_function();
|
|
$content .= "\n"; # for safety
|
|
$content = $t->( $content, \%template_vars );
|
|
|
|
my ( $ref, $parse_error ) = from_toml($content);
|
|
|
|
# only merge it if we have a actual result
|
|
# undef causes the merge feature to wipe it all out
|
|
# that and it did error... so we need to handle the error
|
|
if ( defined($ref) ) {
|
|
|
|
# don't let host variables override the role if
|
|
# roles_merge_after is true
|
|
if ( $self->{roles_merge_after} ) {
|
|
$result = $self->{merger}->merge( $ref, $result );
|
|
}
|
|
else {
|
|
$result = $self->{merger}->merge( $result, $ref );
|
|
}
|
|
}
|
|
else {
|
|
my $error = 'Failed to parse TOML role file "' . $role_file . '" with error... ' . $parse_error;
|
|
if ( $self->{parse_error_fatal} ) {
|
|
die($error);
|
|
}
|
|
else {
|
|
warn($error);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
my $error = "The role '" . $role . "' is specified by the file '" . $role_file . "' does not eixst";
|
|
if ( $self->{missing_role_fatal} ) {
|
|
die($error);
|
|
}
|
|
else {
|
|
warn($error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( defined $item ) {
|
|
return $result->{$item};
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
sub _get_cmdb_files {
|
|
my ( $self, $item, $server ) = @_;
|
|
|
|
$server = $self->__get_hostname_for($server);
|
|
|
|
my @files;
|
|
|
|
if ( !ref $self->{path} ) {
|
|
my $env = Rex::Commands::environment();
|
|
my $server_file = "$server.toml";
|
|
my $default_file = 'default.toml';
|
|
@files = (
|
|
File::Spec->join( $self->{path}, $env, $server_file ),
|
|
File::Spec->join( $self->{path}, $env, $default_file ),
|
|
File::Spec->join( $self->{path}, $server_file ),
|
|
File::Spec->join( $self->{path}, $default_file ),
|
|
);
|
|
}
|
|
elsif ( ref $self->{path} eq "CODE" ) {
|
|
@files = $self->{path}->( $self, $item, $server );
|
|
}
|
|
elsif ( ref $self->{path} eq "ARRAY" ) {
|
|
@files = @{ $self->{path} };
|
|
}
|
|
|
|
my $os = Rex::Hardware::Host->get_operating_system();
|
|
|
|
@files = map { $self->_parse_path( $_, { hostname => $server, operatingsystem => $os, } ) } @files;
|
|
|
|
return @files;
|
|
}
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
Rex::CMDB::TOML - TOML-based CMDB provider for Rex
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This module collects and merges data from a set of TOML files to provide configuration
|
|
management database for Rex.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
use Rex::CMDB;
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => [ 'cmdb/{hostname}.toml', 'cmdb/default.toml', ],
|
|
merge_behavior => 'LEFT_PRECEDENT',
|
|
};
|
|
|
|
task 'prepare', 'server1', sub {
|
|
my $all_information = get cmdb;
|
|
my $specific_item = get cmdb('item');
|
|
my $specific_item_for_server = get cmdb( 'item', 'server' );
|
|
};
|
|
|
|
=head1 CONFIGURATION AND ENVIRONMENT
|
|
|
|
=head2 path
|
|
|
|
The path used to look for CMDB files. It supports various use cases depending on the
|
|
type of data passed to it.
|
|
|
|
=over 4
|
|
|
|
=item * Scalar
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => 'path/to/cmdb',
|
|
};
|
|
|
|
If a scalar is used, it tries to look up a few files under the given path:
|
|
|
|
path/to/cmdb/{environment}/{hostname}.toml
|
|
path/to/cmdb/{environment}/default.toml
|
|
path/to/cmdb/{hostname}.toml
|
|
path/to/cmdb/default.toml
|
|
|
|
=item * Array reference
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => [ 'cmdb/{hostname}.yml', 'cmdb/default.yml', ],
|
|
};
|
|
|
|
If an array reference is used, it tries to look up the mentioned files in the given
|
|
order.
|
|
|
|
=item * Code reference
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => sub {
|
|
my ( $provider, $item, $server ) = @_;
|
|
my @files = ( "$server.yml", "$item.yml" );
|
|
return @files;
|
|
},
|
|
};
|
|
|
|
If a code reference is passed, it should return a list of files that would be looked
|
|
up in the same order. The code reference gets the CMDB provider instance, the item,
|
|
and the server as parameters.
|
|
|
|
=back
|
|
|
|
When the L<0.51 feature flag|Rex#0.51> or later is used, the default value of the
|
|
C<path> option is:
|
|
|
|
[qw(
|
|
cmdb/{operatingsystem}/{hostname}.toml
|
|
cmdb/{operatingsystem}/default.toml
|
|
cmdb/{environment}/{hostname}.toml
|
|
cmdb/{environment}/default.toml
|
|
cmdb/{hostname}.toml
|
|
cmdb/default.toml
|
|
)]
|
|
|
|
The path specification supports macros enclosed within curly braces, which are
|
|
dynamically expanded during runtime. By default, the valid macros are L<Rex::Hardware>
|
|
variables, C<{server}> for the server name of the current connection, and C<{environment}>
|
|
for the current environment.
|
|
|
|
Please note that the default environment is, well, C<default>.
|
|
|
|
You can define additional CMDB paths via the C<-O> command line option by using a
|
|
semicolon-separated list of C<cmdb_path=$path> key-value pairs:
|
|
|
|
rex -O 'cmdb_path=cmdb/{domain}.toml;cmdb_path=cmdb/{domain}/{hostname}.toml;' taskname
|
|
|
|
Those additional paths will be prepended to the current list of CMDB paths (so the last one
|
|
specified will get on top, and thus checked first).
|
|
|
|
=head2 merge_behavior
|
|
|
|
This CMDB provider looks up the specified files in order, and returns the requested data. If
|
|
multiple files specify the same data for a given item, then the first instance of the data
|
|
will be returned by default.
|
|
|
|
Rex uses L<Hash::Merge> internally to merge the data found on different levels of the CMDB
|
|
hierarchy. Any merge strategy supported by that module can be specified to override the
|
|
default one. For example one of the built-in strategies:
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => 'cmdb',
|
|
merge_behavior => 'LEFT_PRECEDENT',
|
|
};
|
|
|
|
Or even custom ones:
|
|
|
|
set cmdb => {
|
|
type => 'TOML',
|
|
path => 'cmdb',
|
|
merge_behavior => {
|
|
SCALAR => sub {},
|
|
ARRAY => sub {},
|
|
HASH => sub {},
|
|
};
|
|
|
|
For the full list of options, please see the documentation of Hash::Merge.
|
|
|
|
=head2 use_roles
|
|
|
|
Specifies if roles should be used or not.
|
|
|
|
This value is a Perl boolean and defaults to '0'.
|
|
|
|
=head2 roles_path
|
|
|
|
The path to look for roles under.
|
|
|
|
By default it is 'cmdb/roles'.
|
|
|
|
=head2 parse_error_fatal
|
|
|
|
If it should die or warn upon TOML parsing error.
|
|
|
|
This is a Perl boolean and the default is '1', to die.
|
|
|
|
=head2 missing_role_fatal
|
|
|
|
If a specified role not being able to be found is fatal.
|
|
|
|
This is a Perl boolean and the default is '1', to die.
|
|
|
|
=head2 roles_merge_after
|
|
|
|
If it should merge the roles into the config instead of the default
|
|
of merging the config into the roles.
|
|
|
|
This is a Perl boolean and the default is '0', meaning the config
|
|
will over write anything in the roles with the default merge_behavior
|
|
settings.
|
|
|
|
=head1 ROLES
|
|
|
|
If use_roles has been set to true, when loading a config file, it will check for
|
|
value 'roles' and if that value is a array, it will then go through and look foreach
|
|
of those roles under the roles_path.
|
|
|
|
So lets say we have the config below.
|
|
|
|
foo = "bar"
|
|
ping="no"
|
|
roles = [ 'test' ]
|
|
|
|
It will then load look under the roles_path for the file 'test.toml', which with
|
|
the default settings would be 'cmdb/roles/test.toml'.
|
|
|
|
Lets say we have the the role file set as below.
|
|
|
|
ping = "yes"
|
|
[ping_test]
|
|
misses = 3
|
|
|
|
This means with the value for ping will be 'no' as the default of 'yes' is being
|
|
overriden by the config value.
|
|
|
|
Somethings to keep in mind when using this.
|
|
|
|
1: Don't define a value you intend to use in a role in any of the config files that
|
|
will me merged unless you want it to always override anything a role may import. So
|
|
with like the example above, you would want to avoid putting ping='no' in the default
|
|
toml file and only set it if you want to override that role in like the toml config
|
|
for that host.
|
|
|
|
2: Roles may not include roles. While it won't error or the like, they also won't
|
|
be reeled in.
|
|
|
|
=cut
|