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

Add SNMP scan forking feature #483

Merged
merged 3 commits into from
Sep 19, 2024
Merged
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
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