e7f1126ddb40093735f16884b969e388a4ce9b5e
[freeside.git] / FS / FS / part_export / trango.pm
1 package FS::part_export::trango;
2
3 =head1 FS::part_export::trango
4
5 This export sends SNMP SETs to a router using the Net::SNMP package.  It requires the following custom fields to be defined on a router.  If any of the required custom fields are not present, then the export will exit quietly.
6
7 =head1 Required custom fields
8
9 =over 4
10
11 =item trango_address - IP address (or hostname) of the Trango AP.
12
13 =item trango_comm - R/W SNMP community of the Trango AP.
14
15 =item trango_ap_type - Trango AP Model.  Currently 'access5830' is the only supported option.
16
17 =back
18
19 =head1 Optional custom fields
20
21 =over 4
22
23 =item trango_baseid - Base ID of the Trango AP.  See L</"Generating SU IDs">.
24
25 =item trango_apid - AP ID of the Trango AP.  See L</"Generating SU IDs">.
26
27 =back
28
29 =head1 Generating SU IDs
30
31 This export will/must generate a unique SU ID for each service exported to a Trango AP.  It can be done such that SU IDs are globally unique, unique per Base ID, or unique per Base ID/AP ID pair.  This is accomplished by setting neither trango_baseid and trango_apid, only trango_baseid, or both trango_baseid and trango_apid, respectively.  An SU ID will be generated if the FS::svc_broadband virtual field specified by suid_field export option is unset, otherwise the existing value will be used.
32
33 =head1 Device Support
34
35 This export has been tested with the Trango Access5830 AP.
36
37
38 =cut
39
40
41 use strict;
42 use vars qw(@ISA %info $me $DEBUG $trango_mib $counter_dir);
43
44 use FS::UID qw(dbh datasrc);
45 use FS::Record qw(qsearch qsearchs);
46 use FS::part_export::snmp;
47
48 use Tie::IxHash;
49 use File::CounterFile;
50 use Data::Dumper qw(Dumper);
51
52 @ISA = qw(FS::part_export::snmp);
53
54 tie my %options, 'Tie::IxHash', (
55   'suid_field' => {
56     'label'   => 'Trango SU ID field',
57     'default' => 'trango_suid',
58     'notes'   => 'Name of the FS::svc_broadband virtual field that will contain the SU ID.',
59   },
60   'mac_field' => {
61     'label'   => 'Trango MAC address field',
62     'default' => '',
63     'notes'   => 'Name of the FS::svc_broadband virtual field that will contain the SU\'s MAC address.',
64   },
65 );
66
67 %info = (
68   'svc'     => 'svc_broadband',
69   'desc'    => 'Sends SNMP SETs to a Trango AP.',
70   'options' => \%options,
71   'notes'   => 'Requires Net::SNMP.  See the documentation for FS::part_export::trango for required virtual fields and usage information.',
72 );
73
74 $me= '[' .  __PACKAGE__ . ']';
75 $DEBUG = 1;
76
77 $trango_mib = {
78   'access5830' => {
79     'snmpversion' => 'snmpv1',
80     'varbinds' => {
81       'insert' => [
82         { # sudbDeleteOrAddID
83           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
84           'type' => 'INTEGER',
85           'value' => \&_trango_access5830_sudbDeleteOrAddId,
86         },
87         { # sudbAddMac
88           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
89           'type' => 'HEX_STRING',
90           'value' => \&_trango_access5830_sudbAddMac,
91         },
92         { # sudbAddSU
93           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
94           'type' => 'INTEGER',
95           'value' => 1,
96         },
97       ],
98       'delete' => [
99         { # sudbDeleteOrAddID
100           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
101           'type' => 'INTEGER',
102           'value' => \&_trango_access5830_sudbDeleteOrAddId,
103         },
104         { # sudbDeleteSU
105           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
106           'type' => 'INTEGER',
107           'value' => 1,
108         },
109       ],
110       'replace' => [
111         { # sudbDeleteOrAddID
112           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
113           'type' => 'INTEGER',
114           'value' => \&_trango_access5830_sudbDeleteOrAddId,
115         },
116         { # sudbDeleteSU
117           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
118           'type' => 'INTEGER',
119           'value' => 1,
120         },
121         { # sudbDeleteOrAddID
122           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
123           'type' => 'INTEGER',
124           'value' => \&_trango_access5830_sudbDeleteOrAddId,
125         },
126         { # sudbAddMac
127           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
128           'type' => 'HEX_STRING',
129           'value' => \&_trango_access5830_sudbAddMac,
130         },
131         { # sudbAddSU
132           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
133           'type' => 'INTEGER',
134           'value' => 1,
135         },
136       ],
137       'suspend' => [
138         { # sudbDeleteOrAddID
139           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
140           'type' => 'INTEGER',
141           'value' => \&_trango_access5830_sudbDeleteOrAddId,
142         },
143         { # sudbDeleteSU
144           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
145           'type' => 'INTEGER',
146           'value' => 1,
147         },
148       ],
149       'unsuspend' => [
150         { # sudbDeleteOrAddID
151           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
152           'type' => 'INTEGER',
153           'value' => \&_trango_access5830_sudbDeleteOrAddId,
154         },
155         { # sudbAddMac
156           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
157           'type' => 'HEX_STRING',
158           'value' => \&_trango_access5830_sudbAddMac,
159         },
160         { # sudbAddSU
161           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
162           'type' => 'INTEGER',
163           'value' => 1,
164         },
165       ],
166     },
167   },
168 };
169
170
171 sub _field_prefix { 'trango'; }
172
173 sub _req_router_fields {
174   map {
175     $_[0]->_field_prefix . '_' . $_
176   } (qw(address comm ap_type suid_field));
177 }
178
179 sub _get_cmd_sub {
180
181   return('FS::part_export::snmp::snmp_cmd');
182
183 }
184
185 sub _prepare_args {
186
187   my ($self, $action, $router) = (shift, shift, shift);
188   my ($svc_broadband) = shift;
189   my $old = shift if $action eq 'replace';
190   my $field_prefix = $self->_field_prefix;
191   my $error;
192
193   my $ap_type = $router->getfield($field_prefix . '_ap_type');
194
195   unless (exists $trango_mib->{$ap_type}) {
196     return "Unsupported Trango AP type '$ap_type'";
197   }
198
199   $error = $self->_check_suid(
200     $action, $router, $svc_broadband, ($old) ? $old : ()
201   );
202   return $error if $error;
203
204   $error = $self->_check_mac(
205     $action, $router, $svc_broadband, ($old) ? $old : ()
206   );
207   return $error if $error;
208
209   my $ap_mib = $trango_mib->{$ap_type};
210
211   my $args = [
212     '-hostname' => $router->getfield($field_prefix.'_address'),
213     '-version' => $ap_mib->{'snmpversion'},
214     '-community' => $router->getfield($field_prefix.'_comm'),
215   ];
216
217   my @varbindlist = ();
218
219   foreach my $oid (@{$ap_mib->{'varbinds'}->{$action}}) {
220     warn "[debug]$me Processing OID '" . $oid->{'oid'} . "'" if $DEBUG;
221     my $value;
222     if (ref($oid->{'value'}) eq 'CODE') {
223       eval {
224         $value = &{$oid->{'value'}}(
225           $self, $action, $router, $svc_broadband,
226           (($old) ? $old : ()),
227         );
228       };
229       return "While processing OID '" . $oid->{'oid'} . "':" . $@
230         if $@;
231     } else {
232       $value = $oid->{'value'};
233     }
234
235     warn "[debug]$me Value for OID '" . $oid->{'oid'} . "': " if $DEBUG;
236
237     if (defined $value) { # Skip OIDs with undefined values.
238       push @varbindlist, ($oid->{'oid'}, $oid->{'type'}, $value);
239     }
240   }
241
242
243   push @$args, ('-varbindlist', @varbindlist);
244   
245   return('', $args);
246
247 }
248
249 sub _check_suid {
250
251   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
252   my $old = shift if $action eq 'replace';
253   my $error;
254
255   my $suid_field = $self->option('suid_field');
256   unless (grep {$_ eq $suid_field} $svc_broadband->fields) {
257     return "Missing Trango SU ID field.  "
258       . "See the trango export options for more info.";
259   }
260
261   my $suid = $svc_broadband->getfield($suid_field);
262   if ($action eq 'replace') {
263     my $old_suid = $old->getfield($suid_field);
264
265     if ($old_suid ne '' and $old_suid ne $suid) {
266       return 'Cannot change Trango SU ID';
267     }
268   }
269
270   if (not $suid =~ /^\d+$/ and $action ne 'delete') {
271     my $new_suid = eval { $self->_get_next_suid($router); };
272     return "Error while getting next Trango SU ID: $@" if ($@);
273
274     warn "[debug]$me Got new SU ID: $new_suid" if $DEBUG;
275     $svc_broadband->set($suid_field, $new_suid);
276
277     #FIXME: Probably a bad hack.
278     #       We need to update the SU ID field in the database.
279
280     my $oldAutoCommit = $FS::UID::AutoCommit;
281     local $FS::svc_Common::noexport_hack = 1;
282     local $FS::UID::AutoCommit = 0;
283     my $dbh = dbh;
284
285     my $svcnum = $svc_broadband->svcnum;
286
287     my $old_svc = qsearchs('svc_broadband', { svcnum => $svcnum });
288     unless ($old_svc) {
289       return "Unable to retrieve svc_broadband with svcnum '$svcnum";
290     }
291
292     my $svcpart = $svc_broadband->svcpart
293       ? $svc_broadband->svcpart
294       : $svc_broadband->cust_svc->svcpart;
295
296     my $new_svc = new FS::svc_broadband {
297       $old_svc->hash,
298       $suid_field => $new_suid,
299       svcpart => $svcpart,
300     };
301
302     $error = $new_svc->check;
303     if ($error) {
304       $dbh->rollback if $oldAutoCommit;
305       return "Error while updating the Trango SU ID: $error" if $error;
306     }
307
308     warn "[debug]$me Updating svc_broadband with SU ID '$new_suid'...\n" .
309       &Dumper($new_svc) if $DEBUG;
310
311     $error = eval { $new_svc->replace($old_svc); };
312
313     if ($@ or $error) {
314       $error ||= $@;
315       $dbh->rollback if $oldAutoCommit;
316       return "Error while updating the Trango SU ID: $error" if $error;
317     }
318
319     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
320
321   }
322
323   return '';
324
325 }
326
327 sub _check_mac {
328
329   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
330   my $old = shift if $action eq 'replace';
331
332   my $mac_field = $self->option('mac_field');
333   unless (grep {$_ eq $mac_field} $svc_broadband->fields) {
334     return "Missing Trango MAC address field.  "
335       . "See the trango export options for more info.";
336   }
337
338   my $mac_addr = $svc_broadband->getfield($mac_field);
339   unless (length(join('', $mac_addr =~ /[0-9a-fA-F]/g)) == 12) {
340     return "Invalid Trango MAC address: $mac_addr";
341   }
342
343   return('');
344
345 }
346
347 sub _get_next_suid {
348
349   my ($self, $router) = (shift, shift);
350
351   my $counter_dir = '/usr/local/etc/freeside/export.'. datasrc . '/trango';
352   my $baseid = $router->getfield('trango_baseid');
353   my $apid = $router->getfield('trango_apid');
354
355   my $counter_file_suffix = '';
356   if ($baseid ne '') {
357     $counter_file_suffix .= "_B$baseid";
358     if ($apid ne '') {
359       $counter_file_suffix .= "_A$apid";
360     }
361   }
362
363   my $counter_file = $counter_dir . '/SUID' . $counter_file_suffix;
364
365   warn "[debug]$me Using SUID counter file '$counter_file'";
366
367   my $suid = eval {
368     mkdir $counter_dir, 0700 unless -d $counter_dir;
369
370     my $cf = new File::CounterFile($counter_file, 0);
371     $cf->inc;
372   };
373
374   die "Error generating next Trango SU ID: $@" if (not $suid or $@);
375
376   return($suid);
377
378 }
379
380
381
382 # Trango-specific subroutines for generating varbind values.
383 #
384 # All subs should die on error, and return undef to decline.  OIDs that
385 # decline will not be added to varbinds.
386
387 sub _trango_access5830_sudbDeleteOrAddId {
388
389   my ($self, $action, $router) = (shift, shift, shift);
390   my ($svc_broadband) = shift;
391   my $old = shift if $action eq 'replace';
392
393   my $suid = $svc_broadband->getfield($self->option('suid_field'));
394
395   # Sanity check.
396   unless ($suid =~ /^\d+$/) {
397     if ($action eq 'delete') {
398       # Silently ignore.  If we don't have a valid SU ID now, we probably
399       # never did.
400       return undef;
401     } else {
402       die "Invalid Trango SU ID '$suid'";
403     }
404   }
405
406   return ($suid);
407
408 }
409
410 sub _trango_access5830_sudbAddMac {
411
412   my ($self, $action, $router) = (shift, shift, shift);
413   my ($svc_broadband) = shift;
414   my $old = shift if $action eq 'replace';
415
416   my $mac_addr = $svc_broadband->getfield($self->option('mac_field'));
417   $mac_addr = join('', $mac_addr =~ /[0-9a-fA-F]/g);
418
419   # Sanity check.
420   die "Invalid Trango MAC address '$mac_addr'" unless (length($mac_addr)==12);
421
422   return($mac_addr);
423
424 }
425
426
427 =head1 BUGS
428
429 Plenty, I'm sure.
430
431 =cut
432
433
434 1;