Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow exporting process to run arbitrary hooks #49

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions etc/Default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,9 @@ RANCID_TYPE_MAP => {
'netscreen' => 'netscreen',
},

# Directory that contains export modules and their corresponding hooks
EXPORTER_HOOKS_DIR => '<<Make:PREFIX>>/etc/exporter/hooks',

#####################################################################
# - BIND - www.isc.org
#
Expand Down
3 changes: 2 additions & 1 deletion etc/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ include $(SRCROOT)/etc/utility-Makefile
#
# makefile for etc/


NDIR = exporter exporter/hooks
FILES = Default.conf netdot_apache2_radius.conf netdot_apache2_ldap.conf netdot_apache2_local.conf netdot_apache24_local.conf netdot.meta

all:
$(mkdirs)
$(substitute)
if ! test -r $(PREFIX)/$(DIR)/Site.conf; then \
$(SED) -r $(REPLACEMENT_EXPRESSIONS) Site.conf \
Expand Down
10 changes: 9 additions & 1 deletion htdocs/export/config_tasks.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@

<%perl>
if ( $submit ){
my $person = $ui->get_user_person($user);
unless ( $manager && $manager->can($user, 'access_admin_section', 'Export:Submit_Configuration') ){
$m->comp('/generic/error.mhtml', error=>"You don't have permission to perform this operation")
}
Expand All @@ -130,7 +131,14 @@
$dhcp_logger->add_appender($logstr);

foreach my $type ( @config_types ){
my %args;
my %args = (
user => {
person_username => $person->username,
person_firstname => $person->firstname,
person_lastname => $person->lastname,
person_email => $person->email,
},
);
if ( $type eq 'BIND' ){
$args{zone_ids} = \@zones if ( scalar @zones && $zones[0] ne "" );
$args{force} = 1 if ($bind_force);
Expand Down
140 changes: 140 additions & 0 deletions lib/Netdot/Exporter.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use warnings;
use strict;
use Data::Dumper;
use Fcntl qw(:DEFAULT :flock);
use JSON;
use IPC::Open3;
use File::Spec;
use Symbol 'gensym';

use File::Spec::Functions qw(catpath);

my $logger = Netdot->log->get_logger('Netdot::Exporter');

Expand Down Expand Up @@ -308,6 +314,140 @@ sub print_eof {
print $fh "\n#### EOF ####\n";
}

########################################################################

=head2 get_short_type - Return the "short" type that this module is.

Arguments:
None
Returns:
Short type. i.e. "Nagios" or "DHCPD" or etc.

=cut

sub get_short_type {
my ($self) = @_;

my $long_type = ref($self);

$logger->trace("Getting short type for $long_type.");

my %long_to_short_lookup = reverse %types;

if (exists $long_to_short_lookup{$long_type}) {
my $short_type = $long_to_short_lookup{$long_type};
$logger->debug("Short type for $long_type is $short_type.");
return $short_type;
}
else {
$self->throw_fatal("Netdot::Exporter::get_short_type: No mapping found for long type ($long_type).");
}
}

########################################################################

=head2 hook - Run the hooks for this point in the export process

Arguments:
Hash of arguments containing:
name
data
keys. Name is a string that will define which hook programs will run at
various points in the exporting process. Data is arbitrary data passed
to the hook program as a JSON encoded string. Usually "data" is a hash
reference.
Returns:
Nothing.

=cut

sub hook {
my $self = shift;
my %args = (
name => undef,
data => {},
@_,
);

my $name = $args{name};
my $data = $args{data};

if (! defined $name) {
$self->throw_fatal("Netdot::Exporter::hook Name of hook is not defined.");
}

# Always include the Netdot name for the external program to consume.
$data->{netdot_name} = Netdot->config->get('NETDOTNAME');

my $hooks_dir = Netdot->config->get('EXPORTER_HOOKS_DIR');
$logger->trace("Configuration set hooks dir to $hooks_dir.");

my $short_type = $self->get_short_type;

my $module_hook_directory = catpath(undef, $hooks_dir, $short_type);
$logger->trace("Netdot::Exporter::hook for $short_type with name $name has a module_hook_directory of: $module_hook_directory");

my $named_hook_directory = catpath(undef, $module_hook_directory, $name);
$logger->trace("Netdot::Exporter::hook for $short_type with name $name has a named_hook_directory of: $named_hook_directory");

open(my $dev_null, '<', File::Spec->devnull) or die $!;

if (-e $named_hook_directory) {
if (-d $named_hook_directory) {
opendir(my $dh, $named_hook_directory) or die;
my @files = sort readdir $dh;
for (@files) {
my $entry = catpath(undef, $named_hook_directory, $_);
if (-f $entry && -x $entry) {
$logger->debug("Found executable file $entry for hook $short_type with name $name.");

# There were issues with the open3 call not being able to
# read from the $output filehandle. The following two
# lines seemed to fix it.
# See:
# http://stackoverflow.com/questions/23770338/perl-embperl-ipcopen3
local *STDOUT;
open(STDOUT, '>&=', 1) or die $!;

my $pid = open3(
$dev_null,
my $output, # autovivified filehandle to read from
my $error = gensym, # we can't autovivify stderr filehandle, so use gensym
$entry, # the actual program to run
encode_json($data), # and the arguments to pass on the command line
) or die $!;

while (<$output>) {
chomp;
$logger->info("$_");
}

while (<$error>) {
chomp;
$logger->error("$_ [from: hook $short_type:$name $entry]");
}

waitpid($pid, 0);

my $child_exit_status = $? >> 8;
if ($child_exit_status != 0) {
$logger->warn("$entry had an exit status of: $child_exit_status");
}

close $output or die $!;
close $error or die $!;
}
}
closedir $dh or die;
}
else {
$logger->warn("$named_hook_directory exists, but is not a directory.");
}
}

close $dev_null or die $!;
}

=head1 AUTHORS

Carlos Vicente, C<< <cvicente at ns.uoregon.edu> >>
Expand Down
38 changes: 36 additions & 2 deletions lib/Netdot/Exporter/BIND.pm
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ sub generate_configs {
my ($self, %argv) = @_;

my @zones;

if ( $argv{zones} ){
unless ( ref($argv{zones}) eq 'ARRAY' ){
$self->throw_fatal("zones argument must be arrayref!");
Expand All @@ -84,7 +84,15 @@ sub generate_configs {
}else{
@zones = Zone->retrieve_all();
}


$self->hook(
name => 'before-all-zones-written',
data => {
user => $argv{user},
},
);

my @written_zones = ();
foreach my $zone ( @zones ){
next unless $zone->active;
eval {
Expand All @@ -100,6 +108,23 @@ sub generate_configs {
$record->update({pending=>0});
}
$logger->info("Zone ".$zone->name." written to file: $path");
my %data = (
zone_name => $zone->name,
path => $path,
);

my %copy_of_data = %data;

# save a copy so we can send the aggregate to a later "hook".
push @written_zones, \%copy_of_data;

# add user data in case the hook'ed programs want to use it.
$data{user} = $argv{user};

$self->hook(
name => 'after-zone-written',
data => \%data,
);
}else{
$logger->debug("Exporter::BIND::generate_configs: ".$zone->name.
": No pending changes. Use -f to force.");
Expand All @@ -108,6 +133,15 @@ sub generate_configs {
};
$logger->error($@) if $@;
}

my $data = {
zones_written => \@written_zones,
user => $argv{user},
};
$self->hook(
name => 'after-all-zones-written',
data => $data,
);
}

############################################################################
Expand Down
41 changes: 40 additions & 1 deletion lib/Netdot/Exporter/DHCPD.pm
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ sub generate_configs {
}
}
}

$self->hook(
name => 'before-all-scopes-written',
data => {
user => $argv{user},
},
);

my @written_scopes = ();
foreach my $s ( @gscopes ){
Netdot::Model->do_transaction(sub{
if ( (my @pending = HostAudit->search(scope=>$s->name, pending=>1)) || $argv{force} ){
Expand All @@ -76,13 +85,43 @@ sub generate_configs {
# Un-mark audit records as pending
$record->update({pending=>0});
}
$s->print_to_file();
my $path = $s->print_to_file();

# Only perform "hook" things if we actually wrote out a file...
if (defined $path) {
my %data = (
scope_name => $s->name,
path => $path,
);

my %copy_of_data = %data;

# save a copy so we can send the aggregate to a later "hook".
push @written_scopes, \%copy_of_data;

# add user data in case the hook'ed programs want to use it.
$data{user} = $argv{user};

$self->hook(
name => 'after-scope-written',
data => \%data,
);
}
}else{
$logger->debug("Exporter::DHCPD::generate_configs: ".$s->name.
": No pending changes. Use -f to force.");
}
});
}

my $data = {
scopes_written => \@written_scopes,
user => $argv{user},
};
$self->hook(
name => 'after-all-scopes-written',
data => $data,
);
}

=head1 AUTHOR
Expand Down
6 changes: 4 additions & 2 deletions lib/Netdot/Model/DhcpScope.pm
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,8 @@ sub delete{
Hash with following keys:
filename - (Optional)
Returns:
True
Path of file written to if successfully written
undef if scope is not active
Examples:
$scope->print_to_file();

Expand All @@ -293,7 +294,7 @@ sub print_to_file{
unless ( $self->active ){
$logger->info(sprintf("DhcpScope::print_to_file: Scope %s is marked ".
"as not active. Aborting", $self->get_label));
return;
return undef;
}

my $start = time;
Expand Down Expand Up @@ -329,6 +330,7 @@ sub print_to_file{
$logger->info(sprintf("DHCPD Scope %s exported to %s, in %s",
$self->name, $path, $class->sec2dhms($end-$start) ));

return $path;
}


Expand Down