Skip to content

Commit

Permalink
Merge pull request #483 from OCSInventory-NG/snmpForking
Browse files Browse the repository at this point in the history
[DEV] Add SNMP scan forking feature
  • Loading branch information
charleneauger authored Sep 19, 2024
2 parents 76b8abf + 16b3ca6 commit 9f0d835
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 28 deletions.
1 change: 1 addition & 0 deletions etc/ocsinventory-agent/modules.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Ocsinventory::Agent::Modules::Download;
use Ocsinventory::Agent::Modules::SnmpScan;
#use Ocsinventory::Agent::Modules::LocalSnmpScan;
use Ocsinventory::Agent::Modules::SnmpFork;

# DO NOT REMOVE the 1;
1;
129 changes: 129 additions & 0 deletions lib/Ocsinventory/Agent/Modules/SnmpFork.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
###############################################################################
## OCSINVENTORY-NG
## Copyleft OCS Inventory NG Team
## Web : http://www.ocsinventory-ng.org
##
## Wrapper for SNMP scan (local and online mode) that handles the forking of the
## SNMP scan process
##
## This code is open source and may be copied and modified as long as the source
## code is always made freely available.
## Please refer to the General Public Licence http://www.gnu.org/ or Licence.txt
################################################################################

package Ocsinventory::Agent::Modules::SnmpFork;

use strict;
no strict 'refs';
no strict 'subs';
use warnings;

use XML::Simple;
use Digest::MD5;


# launch the SNMP scan in a forked process
# takes the native scan function to call, subnets to scan, nb of forks and self
sub fork_snmpscan {
my ($scan_function, $nets_to_scan, $fork_count, $self) = @_;

my $logger = $self->{logger};

# get fork count from config or calculate it
$fork_count = $fork_count // 0;
if ($fork_count !~ /^\d+$/ || $fork_count <= 0) {
$logger->debug("Invalid fork_nb value in config: $fork_count. Falling back to calculated value.");
$fork_count = get_forks_nb();
}

# split nets_to_scan among forks
my @split_nets_to_scan = split_array_across_forks($nets_to_scan, $fork_count);

my @pipes;
my @aggregated_content;

# fork processes
for (my $i = 0; $i < $fork_count; $i++) {
# pipe
my ($reader, $writer);
pipe($reader, $writer);
$reader->autoflush(1);
$writer->autoflush(1);
push @pipes, $reader;

my $pid = fork();
if ($pid) {
# parent
close $writer;
} elsif (defined $pid) {
# child
close $reader;
my $subnets_to_scan = $split_nets_to_scan[$i];

# calling scan function
my $xml_result = $scan_function->($self, $subnets_to_scan);

# write xml result to pipe
print $writer $xml_result;
close $writer;
exit 0;
} else {
$logger->error("Fork failed: $!");
}
}

# parent process: read and aggregate XML from pipes
foreach my $reader (@pipes) {
while (my $line = <$reader>) {
push @aggregated_content, $line;
}
close $reader;
}

# wait for all child processes to finish
my $child_pid;
while (($child_pid = waitpid(-1, 0)) > 0) {
$logger->debug("Child process $child_pid finished with exit code $?");
}

# aggregated content into one content block
my $content_block = join("", @aggregated_content);

# final XML structure
my $final_xml = <<"END_XML";
<?xml version="1.0" encoding="UTF-8"?>
<REQUEST>
<CONTENT>
$content_block
</CONTENT>
<DEVICEID>$self->{context}->{config}->{deviceid}</DEVICEID>
<QUERY>SNMP</QUERY>
</REQUEST>
END_XML

return $final_xml;
}

# split the array of IPs into even portions for each fork
sub split_array_across_forks {
my ($nets_to_scan, $fork_count) = @_;
my @split_nets;

my $i = 0;
foreach my $subnet (@$nets_to_scan) {
push(@{$split_nets[$i]}, $subnet);
$i = ($i + 1) % $fork_count;
}

return @split_nets;
}

# default nb of forks is nb of cores
sub get_forks_nb {
my $cores = `nproc`;
chomp($cores);
return $cores;
}


1;
111 changes: 83 additions & 28 deletions lib/Ocsinventory/Agent/Modules/SnmpScan.pm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
################################################################################

package Ocsinventory::Agent::Modules::SnmpScan;
use Ocsinventory::Agent::Modules::SnmpFork ();

use strict;
no strict 'refs';
Expand Down Expand Up @@ -55,6 +56,12 @@ sub snmpscan_start_handler {

$logger->debug("Calling snmp_start_handler");

if ($config->{forking_enabled}) {
$logger->debug("SNMP Forking is enabled");
} else {
$logger->debug("SNMP Forking is disabled");
}

# Disabling module if local mode
if ($config->{stdout} || $config->{local}) {
$self->{disabled} = 1;
Expand Down Expand Up @@ -154,23 +161,7 @@ sub snmpscan_end_handler {

# We get the config
my $config = $self->{context}->{config};
# Load setting from the config file
my $configagent = new Ocsinventory::Agent::Config;
$configagent->loadUserParams();

my $communities=$self->{communities};

if ( ! defined ($communities ) ) {
$logger->debug("We have no Community from server, we use default public community");
$communities=[{VERSION=>"2c",NAME=>"public"}];
}

my ($name,$comm,$error,$system_oid);

# Initalising the XML properties
my $snmp_inventory = $self->{inventory};
$snmp_inventory->{xmlroot}->{QUERY} = ['SNMP'];
$snmp_inventory->{xmlroot}->{DEVICEID} = [$self->{context}->{config}->{deviceid}];

# Scanning network
$logger->debug("Snmp: Scanning network");
Expand All @@ -182,15 +173,65 @@ sub snmpscan_end_handler {
my $net_to_scan = [];
$self->snmp_ip_scan($net_to_scan);
} else {
foreach my $net_to_scan ( @$nets_to_scan ){
foreach my $net_to_scan (@$nets_to_scan) {
$self->snmp_ip_scan($net_to_scan);
}
}
$logger->debug("Snmp: Ending Scanning network");

my $xml_inventory;
if ($config->{forking_enabled}) {
$xml_inventory = Ocsinventory::Agent::Modules::SnmpFork::fork_snmpscan(\&perform_snmp_scan, $self->{netdevices}, $config->{fork_count}, $self);
} else {
$xml_inventory = $self->perform_snmp_scan();
}

$self->handleXml($xml_inventory);

$logger->debug("End snmp_end_handler :)");
}

sub perform_snmp_scan {
my $self = shift;
my $logger = $self->{logger};
my $common = $self->{context}->{common};
my $network = $self->{context}->{network};

# Begin scanning ip tables
my $ip=$self->{netdevices};

# identify the process
my $forked = 0;
if ($self->{context}->{config}->{forking_enabled}) {
$forked = 1;
$ip = shift;
}

my $communities=$self->{communities};

if ( ! defined ($communities ) ) {
$logger->debug("We have no Community from server, we use default public community");
$communities=[{VERSION=>"2c",NAME=>"public"}];
}
my ($name,$comm,$error,$system_oid);

# Load setting from the config file
my $configagent = new Ocsinventory::Agent::Config;
$configagent->loadUserParams();

# Initalising the XML properties
my $snmp_inventory = $self->{inventory};
$snmp_inventory->{xmlroot}->{QUERY} = ['SNMP'];
$snmp_inventory->{xmlroot}->{DEVICEID} = [$self->{context}->{config}->{deviceid}];


my $pidlog;
if ($forked) {
$pidlog = "[$$]";
} else {
$pidlog = "";
}

foreach my $device ( @$ip ) {
my $session = undef;
my $oid_condition = undef;
Expand All @@ -200,7 +241,7 @@ sub snmpscan_end_handler {
my $snmp_condition_value = undef;
my $regex = undef;

$logger->debug("Scanning $device->{IPADDR} device");
$logger->debug("$pidlog Scanning device $device->{IPADDR} device");
# Search for the good snmp community in the table community
LIST_SNMP: foreach $comm ( @$communities ) {
# Test if we use SNMP v3
Expand Down Expand Up @@ -272,7 +313,7 @@ sub snmpscan_end_handler {
);
};
unless (defined($session)) {
$logger->error("Snmp INFO: $error");
$logger->error("$pidlog Snmp INFO: $error");
} else {
$self->{snmp_session}=$session;

Expand Down Expand Up @@ -361,17 +402,31 @@ sub snmpscan_end_handler {
}
}

$logger->info("No more SNMP device to scan");

# Formatting the XML and sendig it to the server
my $content = XMLout( $snmp_inventory->{xmlroot}, RootName => 'REQUEST' , XMLDecl => '<?xml version="1.0" encoding="UTF-8"?>', SuppressEmpty => undef );
$logger->info("$pidlog No more SNMP device to scan");
my $clean_content;
my $content;
if ($forked) {
$content = XMLout($snmp_inventory->{xmlroot}, RootName => 'REQUEST', XMLDecl => '<?xml version="1.0" encoding="UTF-8"?>', SuppressEmpty => undef);
$content = extract_content_tag($content);
$logger->debug("$pidlog Sending XML content to parent process");
} else {
# Formatting the XML and sendig it to the server
$content = XMLout( $snmp_inventory->{xmlroot}, RootName => 'REQUEST' , XMLDecl => '<?xml version="1.0" encoding="UTF-8"?>', SuppressEmpty => undef );
}

#Cleaning XML to delete unprintable characters
my $clean_content = $common->cleanXml($content);

$self->handleXml($clean_content);
$clean_content = $common->cleanXml($content);

return $clean_content;
}

$logger->debug("End snmp_end_handler :)");
# extract CONTENT tag (used in perform_snmp_scan for forking)
sub extract_content_tag {
my ($xml_string) = @_;
if ($xml_string =~ m|<CONTENT>(.*?)</CONTENT>|s) {
return $1;
}
return '';
}

sub snmp_ip_scan {
Expand Down Expand Up @@ -402,7 +457,7 @@ sub snmp_ip_scan {
$ping->close();

} elsif ($snmp_scan_type eq 'NMAP' && $common->can_load('Nmap::Parser')) {
$logger->debug("Scannig $net_to_scan with nmap");
$logger->debug("Scanning $net_to_scan with nmap");
my $nmaparser = Nmap::Parser->new;

$nmaparser->parsescan("nmap","-sn",$net_to_scan);
Expand Down
2 changes: 2 additions & 0 deletions postinst.pl
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@
print MODULE "\n";
print MODULE "#use Ocsinventory::Agent::Modules::LocalSnmpScan;\n";
print MODULE "\n";
print MODULE "use Ocsinventory::Agent::Modules::SnmpFork;\n";
print MODULE "\n";
print MODULE "# DO NOT REMOVE THE 1;\n";
print MODULE "1;\n";
close MODULE;
Expand Down

0 comments on commit 9f0d835

Please sign in to comment.