Skip to content

Commit 8eb15b3

Browse files
committed
feat: Add iec61850 protocol support to netdiscovery task
1 parent b01578b commit 8eb15b3

File tree

3 files changed

+324
-0
lines changed

3 files changed

+324
-0
lines changed

lib/GLPI/Agent/IEC61850/Device.pm

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package GLPI::Agent::IEC61850::Device;
2+
3+
use strict;
4+
use warnings;
5+
6+
use UNIVERSAL::require;
7+
8+
use GLPI::Agent::Tools;
9+
use GLPI::Agent::Tools::SNMP;
10+
11+
use GLPI::Agent::IEC61850::Protocol;
12+
13+
use constant discovery => [ qw( SNMPHOSTNAME TYPE )];
14+
use constant inventory => [ qw( INFO )];
15+
16+
my $discovery_infos = {
17+
FIRMWARE => [ qw( PhyNam swRev ) ],
18+
LOCATION => [ qw( PhyNam location ) ],
19+
MODEL => [ qw( PhyNam model ) ],
20+
SERIAL => [ qw( PhyNam serNum ) ],
21+
MANUFACTURER => [ qw( PhyNam vendor ) ],
22+
CONTACT => [ qw( PhyNam owner ) ],
23+
DESCRIPTION => [ qw( Description ) ],
24+
};
25+
26+
sub new {
27+
my ($class, %params) = @_;
28+
29+
my $self = {
30+
logger => $params{logger},
31+
timeout => $params{timeout} // 60,
32+
};
33+
34+
bless $self, $class;
35+
36+
return $self;
37+
}
38+
39+
sub scan {
40+
my ($self, $ip, $port) = @_;
41+
42+
my $protocol = GLPI::Agent::IEC61850::Protocol->new(
43+
timeout => $self->{timeout},
44+
logger => $self->{logger},
45+
);
46+
47+
$protocol->connect($ip, $port)
48+
or return;
49+
50+
$protocol->scan();
51+
52+
$protocol->disconnect();
53+
54+
$self->{protocol} = $protocol;
55+
56+
return $self->getDiscoveryInfo();
57+
}
58+
59+
sub getDiscoveryInfo {
60+
my ($self) = @_;
61+
62+
return unless $self->{protocol};
63+
64+
my $info = {};
65+
66+
# Filter out to only keep discovery infos
67+
foreach my $infokey (sort keys(%{$discovery_infos})) {
68+
my $request = $discovery_infos->{$infokey};
69+
my $value = getCanonicalString($self->{protocol}->getVariable(@{$request}));
70+
next if empty($value);
71+
$info->{$infokey} = $value;
72+
}
73+
74+
# Set type
75+
$info->{TYPE} = "NETWORKING";
76+
77+
return $info;
78+
}
79+
80+
1;
81+
82+
__END__
83+
84+
=head1 NAME
85+
86+
GLPI::Agent::IEC61850::Device - GLPI Agent IEC61850 device
87+
88+
=head1 DESCRIPTION
89+
90+
Class to help handle general methods to apply on a IEC61850 device
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package GLPI::Agent::IEC61850::Protocol;
2+
3+
use strict;
4+
use warnings;
5+
6+
use iec61850;
7+
8+
use GLPI::Agent::Tools;
9+
10+
use constant IED_ERROR_OK => $iec61850::IED_ERROR_OK;
11+
use constant IEC61850_FC_DC => $iec61850::IEC61850_FC_DC;
12+
use constant ACSI_CLASS_DATA_OBJECT => $iec61850::ACSI_CLASS_DATA_OBJECT;
13+
14+
use constant logger_prefix => "[iec61850] ";
15+
16+
use constant PhyNamVariables => [ qw(model hwRev vendor serNum swRev owner location) ];
17+
18+
sub new {
19+
my ($class, %params) = @_;
20+
21+
my $self = {
22+
glpi => $params{glpi} // '', # glpi server version if we need to check feature support
23+
logger => $params{logger},
24+
timeout => $params{timeout} // 60, # In second
25+
ip => $params{ip},
26+
};
27+
28+
bless $self, $class;
29+
30+
return $self;
31+
}
32+
33+
sub connect {
34+
my ($self, $host, $port) = @_;
35+
36+
my $con = iec61850::IedConnection_create();
37+
38+
# Timeout must be set here in millisecond for iec61850 APIs
39+
my $timeout = $self->{timeout} * 1000;
40+
iec61850::IedConnection_setConnectTimeout($con, $timeout);
41+
42+
my $error = iec61850::IedConnection_connect($con, $host, $port || 102);
43+
if ($error != IED_ERROR_OK) {
44+
$self->{logger}->debug(logger_prefix."Connection error: ".iec61850::IedClientError_toString($error));
45+
return 0;
46+
}
47+
48+
# Also configure timeout on requests
49+
iec61850::IedConnection_setRequestTimeout($con, $timeout);
50+
51+
$self->{logger}->debug2(logger_prefix."Connected to $host:".($port || 102));
52+
53+
return $self->{_connection} = $con;
54+
}
55+
56+
sub disconnect {
57+
my ($self) = @_;
58+
59+
iec61850::IedConnection_close(delete $self->{_connection})
60+
if defined($self->{_connection});
61+
}
62+
63+
sub scan {
64+
my ($self) = @_;
65+
66+
my $maxDevices = 1;
67+
68+
my $error = iec61850::IedConnection_getDeviceModelFromServer($self->{_connection});
69+
if ($error != IED_ERROR_OK) {
70+
$self->{logger}->debug(logger_prefix."getDeviceModelFromServer error: ".iec61850::IedClientError_toString($error));
71+
return;
72+
}
73+
74+
my $deviceList;
75+
($deviceList, $error) = iec61850::IedConnection_getServerDirectory($self->{_connection}, 0);
76+
if ($error != IED_ERROR_OK) {
77+
$self->{logger}->debug(logger_prefix."getServerDirectory error: ".iec61850::IedClientError_toString($error));
78+
return;
79+
} elsif (!defined($deviceList)) {
80+
$self->{logger}->debug2(logger_prefix."No data returned from device");
81+
return;
82+
}
83+
84+
my $device = iec61850::LinkedList_getNext($deviceList);
85+
while (defined($device)) {
86+
my $name = iec61850::toCharP($device->swig_data_get);
87+
$self->{logger}->debug2(logger_prefix."Scanning $name device");
88+
$self->_getLogicalDeviceDirectory($name);
89+
last unless --$maxDevices;
90+
$device = iec61850::LinkedList_getNext($device);
91+
}
92+
93+
iec61850::LinkedList_destroy($deviceList);
94+
}
95+
96+
sub _getLogicalDeviceDirectory {
97+
my ($self, $device) = @_;
98+
99+
# Keep found device as description
100+
$self->{Description} = $device;
101+
102+
my ($logicalNodes, $error) = iec61850::IedConnection_getLogicalDeviceDirectory($self->{_connection}, $device);
103+
if ($error != IED_ERROR_OK) {
104+
$self->{logger}->debug(logger_prefix."getLogicalDeviceDirectory error: ".iec61850::IedClientError_toString($error));
105+
return;
106+
} elsif (!defined($logicalNodes)) {
107+
$self->{logger}->debug(logger_prefix."Failed to get $device logical device");
108+
return;
109+
}
110+
111+
my $logicalNode = iec61850::LinkedList_getNext($logicalNodes);
112+
while (defined($logicalNode)) {
113+
my $lnName = iec61850::toCharP($logicalNode->swig_data_get);
114+
if ($lnName =~ /^LPHD\d+$/) {
115+
$self->{logger}->debug2(logger_prefix."Scanning $device/$lnName logical node directory");
116+
$self->_getLogicalNodeDirectory("$device/$lnName");
117+
# No need to continue on next logicalNode as we reached the one with required datas
118+
last;
119+
}
120+
$logicalNode = iec61850::LinkedList_getNext($logicalNode);
121+
}
122+
123+
iec61850::LinkedList_destroy($logicalNodes);
124+
}
125+
126+
sub _getLogicalNodeDirectory {
127+
my ($self, $logicalNode) = @_;
128+
129+
my ($dataObjects, $error) = iec61850::IedConnection_getLogicalNodeDirectory($self->{_connection}, $logicalNode, ACSI_CLASS_DATA_OBJECT);
130+
if ($error != IED_ERROR_OK) {
131+
$self->{logger}->debug(logger_prefix."getLogicalNodeDirectory error: ".iec61850::IedClientError_toString($error));
132+
return;
133+
} elsif (!defined($dataObjects)) {
134+
$self->{logger}->debug(logger_prefix."Failed to get $logicalNode logical node");
135+
return;
136+
}
137+
138+
my $dataObject = iec61850::LinkedList_getNext($dataObjects);
139+
while (defined($dataObject)) {
140+
my $dataObjectName = iec61850::toCharP($dataObject->swig_data_get);
141+
if ($dataObjectName eq "PhyNam") {
142+
$self->_getVariables($logicalNode, $dataObjectName, PhyNamVariables);
143+
# No need to continue on next dataObject as we reached the one with required datas
144+
last;
145+
}
146+
$dataObject = iec61850::LinkedList_getNext($dataObject);
147+
}
148+
149+
iec61850::LinkedList_destroy($dataObjects);
150+
}
151+
152+
sub _getVariables {
153+
my ($self, $logicalNode, $dataObject, $variables) = @_;
154+
155+
my $dataObjectVariables = $logicalNode.".".$dataObject;
156+
157+
foreach my $var (@{$variables}) {
158+
my $ref = $dataObjectVariables.".".$var;
159+
my ($value, $error) = iec61850::IedConnection_readStringValue($self->{_connection}, $ref, IEC61850_FC_DC);
160+
if ($error != IED_ERROR_OK) {
161+
$self->{logger}->debug(logger_prefix."readStringValue error for $ref: ".iec61850::IedClientError_toString($error));
162+
next;
163+
} elsif (empty($value)) {
164+
# Just skip eventually not defined or empty values
165+
next;
166+
}
167+
$self->{$dataObject}->{$var} = $value;
168+
}
169+
}
170+
171+
sub getVariable {
172+
my ($self, $dataObject, $variable) = @_;
173+
174+
return if empty($dataObject);
175+
176+
return $self->{$dataObject} unless ref($self->{$dataObject}) eq "HASH" && !empty($variable);
177+
178+
return $self->{$dataObject}->{$variable};
179+
}
180+
181+
sub DESTROY {
182+
my ($self) = @_;
183+
184+
iec61850::IedConnection_close($self->{_connection})
185+
if defined($self->{_connection});
186+
}
187+
188+
1;
189+
190+
__END__
191+
192+
=head1 NAME
193+
194+
GLPI::Agent::IEC61850::Device - GLPI Agent IEC61850 device
195+
196+
=head1 DESCRIPTION
197+
198+
Class to help handle general methods to apply on a IEC61850 device

lib/GLPI/Agent/Task/NetDiscovery.pm

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ sub run {
160160
);
161161
}
162162

163+
GLPI::Agent::IEC61850::Device->require();
164+
if ($EVAL_ERROR) {
165+
$self->{logger}->info(
166+
"Can't load GLPI::Agent::IEC61850::Device, iec61850 detection " .
167+
"can't be used"
168+
);
169+
}
170+
163171
# Preload MibSupport
164172
GLPI::Agent::SNMP::MibSupport::preload(
165173
config => $self->{config},
@@ -695,6 +703,7 @@ sub _scanAddress {
695703

696704
# Then scan for standard network datas
697705
%device = (
706+
$INC{'GLPI/Agent/IEC61850/Device.pm'} ? $self->_scanAddressByIEC61850($params) : (),
698707
$INC{'Net/NBName.pm'} ? $self->_scanAddressByNetbios($params) : (),
699708
$INC{'Net/Ping.pm'} ? $self->_scanAddressByPing($params) : (),
700709
$self->{arp} ? $self->_scanAddressByArp($params) : (),
@@ -1049,6 +1058,33 @@ sub _scanAddressByRemote {
10491058
return %device;
10501059
}
10511060

1061+
sub _scanAddressByIEC61850 {
1062+
my ($self, $params) = @_;
1063+
1064+
return if $params->{walk};
1065+
1066+
my $infos;
1067+
eval {
1068+
my $device = GLPI::Agent::IEC61850::Device->new(
1069+
timeout => $params->{timeout} || 1,
1070+
logger => $self->{logger},
1071+
);
1072+
$infos = $device->scan($params->{ip}, $params->{port});
1073+
};
1074+
1075+
return $EVAL_ERROR if $EVAL_ERROR;
1076+
1077+
$self->{logger}->debug(
1078+
sprintf "- scanning %s with iec61850: %s",
1079+
$params->{ip},
1080+
$infos ? 'success' : 'no result'
1081+
);
1082+
1083+
return unless $infos;
1084+
1085+
return %{$infos};
1086+
}
1087+
10521088
sub _sendStartMessage {
10531089
my ($self, $pid) = @_;
10541090

0 commit comments

Comments
 (0)