export host selection per service, RT#17914
[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   'no_machine' => 1,
72   'notes'   => 'Requires Net::SNMP.  See the documentation for FS::part_export::trango for required virtual fields and usage information.',
73 );
74
75 $me= '[' .  __PACKAGE__ . ']';
76 $DEBUG = 1;
77
78 $trango_mib = {
79   'access5830' => {
80     'snmpversion' => 'snmpv1',
81     'varbinds' => {
82       'insert' => [
83         { # sudbDeleteOrAddID
84           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
85           'type' => 'INTEGER',
86           'value' => \&_trango_access5830_sudbDeleteOrAddId,
87         },
88         { # sudbAddMac
89           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
90           'type' => 'HEX_STRING',
91           'value' => \&_trango_access5830_sudbAddMac,
92         },
93         { # sudbAddSU
94           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
95           'type' => 'INTEGER',
96           'value' => 1,
97         },
98       ],
99       'delete' => [
100         { # sudbDeleteOrAddID
101           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
102           'type' => 'INTEGER',
103           'value' => \&_trango_access5830_sudbDeleteOrAddId,
104         },
105         { # sudbDeleteSU
106           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
107           'type' => 'INTEGER',
108           'value' => 1,
109         },
110       ],
111       'replace' => [
112         { # sudbDeleteOrAddID
113           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
114           'type' => 'INTEGER',
115           'value' => \&_trango_access5830_sudbDeleteOrAddId,
116         },
117         { # sudbDeleteSU
118           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
119           'type' => 'INTEGER',
120           'value' => 1,
121         },
122         { # sudbDeleteOrAddID
123           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
124           'type' => 'INTEGER',
125           'value' => \&_trango_access5830_sudbDeleteOrAddId,
126         },
127         { # sudbAddMac
128           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
129           'type' => 'HEX_STRING',
130           'value' => \&_trango_access5830_sudbAddMac,
131         },
132         { # sudbAddSU
133           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
134           'type' => 'INTEGER',
135           'value' => 1,
136         },
137       ],
138       'suspend' => [
139         { # sudbDeleteOrAddID
140           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
141           'type' => 'INTEGER',
142           'value' => \&_trango_access5830_sudbDeleteOrAddId,
143         },
144         { # sudbDeleteSU
145           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
146           'type' => 'INTEGER',
147           'value' => 1,
148         },
149       ],
150       'unsuspend' => [
151         { # sudbDeleteOrAddID
152           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
153           'type' => 'INTEGER',
154           'value' => \&_trango_access5830_sudbDeleteOrAddId,
155         },
156         { # sudbAddMac
157           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
158           'type' => 'HEX_STRING',
159           'value' => \&_trango_access5830_sudbAddMac,
160         },
161         { # sudbAddSU
162           'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
163           'type' => 'INTEGER',
164           'value' => 1,
165         },
166       ],
167     },
168   },
169 };
170
171
172 sub _field_prefix { 'trango'; }
173
174 sub _req_router_fields {
175   map {
176     $_[0]->_field_prefix . '_' . $_
177   } (qw(address comm ap_type suid_field));
178 }
179
180 sub _get_cmd_sub {
181
182   return('FS::part_export::snmp::snmp_cmd');
183
184 }
185
186 sub _prepare_args {
187
188   my ($self, $action, $router) = (shift, shift, shift);
189   my ($svc_broadband) = shift;
190   my $old = shift if $action eq 'replace';
191   my $field_prefix = $self->_field_prefix;
192   my $error;
193
194   my $ap_type = $router->getfield($field_prefix . '_ap_type');
195
196   unless (exists $trango_mib->{$ap_type}) {
197     return "Unsupported Trango AP type '$ap_type'";
198   }
199
200   $error = $self->_check_suid(
201     $action, $router, $svc_broadband, ($old) ? $old : ()
202   );
203   return $error if $error;
204
205   $error = $self->_check_mac(
206     $action, $router, $svc_broadband, ($old) ? $old : ()
207   );
208   return $error if $error;
209
210   my $ap_mib = $trango_mib->{$ap_type};
211
212   my $args = [
213     '-hostname' => $router->getfield($field_prefix.'_address'),
214     '-version' => $ap_mib->{'snmpversion'},
215     '-community' => $router->getfield($field_prefix.'_comm'),
216   ];
217
218   my @varbindlist = ();
219
220   foreach my $oid (@{$ap_mib->{'varbinds'}->{$action}}) {
221     warn "[debug]$me Processing OID '" . $oid->{'oid'} . "'" if $DEBUG;
222     my $value;
223     if (ref($oid->{'value'}) eq 'CODE') {
224       eval {
225         $value = &{$oid->{'value'}}(
226           $self, $action, $router, $svc_broadband,
227           (($old) ? $old : ()),
228         );
229       };
230       return "While processing OID '" . $oid->{'oid'} . "':" . $@
231         if $@;
232     } else {
233       $value = $oid->{'value'};
234     }
235
236     warn "[debug]$me Value for OID '" . $oid->{'oid'} . "': " if $DEBUG;
237
238     if (defined $value) { # Skip OIDs with undefined values.
239       push @varbindlist, ($oid->{'oid'}, $oid->{'type'}, $value);
240     }
241   }
242
243
244   push @$args, ('-varbindlist', @varbindlist);
245   
246   return('', $args);
247
248 }
249
250 sub _check_suid {
251
252   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
253   my $old = shift if $action eq 'replace';
254   my $error;
255
256   my $suid_field = $self->option('suid_field');
257   unless (grep {$_ eq $suid_field} $svc_broadband->fields) {
258     return "Missing Trango SU ID field.  "
259       . "See the trango export options for more info.";
260   }
261
262   my $suid = $svc_broadband->getfield($suid_field);
263   if ($action eq 'replace') {
264     my $old_suid = $old->getfield($suid_field);
265
266     if ($old_suid ne '' and $old_suid ne $suid) {
267       return 'Cannot change Trango SU ID';
268     }
269   }
270
271   if (not $suid =~ /^\d+$/ and $action ne 'delete') {
272     my $new_suid = eval { $self->_get_next_suid($router); };
273     return "Error while getting next Trango SU ID: $@" if ($@);
274
275     warn "[debug]$me Got new SU ID: $new_suid" if $DEBUG;
276     $svc_broadband->set($suid_field, $new_suid);
277
278     #FIXME: Probably a bad hack.
279     #       We need to update the SU ID field in the database.
280
281     my $oldAutoCommit = $FS::UID::AutoCommit;
282     local $FS::svc_Common::noexport_hack = 1;
283     local $FS::UID::AutoCommit = 0;
284     my $dbh = dbh;
285
286     my $svcnum = $svc_broadband->svcnum;
287
288     my $old_svc = qsearchs('svc_broadband', { svcnum => $svcnum });
289     unless ($old_svc) {
290       return "Unable to retrieve svc_broadband with svcnum '$svcnum";
291     }
292
293     my $svcpart = $svc_broadband->svcpart
294       ? $svc_broadband->svcpart
295       : $svc_broadband->cust_svc->svcpart;
296
297     my $new_svc = new FS::svc_broadband {
298       $old_svc->hash,
299       $suid_field => $new_suid,
300       svcpart => $svcpart,
301     };
302
303     $error = $new_svc->check;
304     if ($error) {
305       $dbh->rollback if $oldAutoCommit;
306       return "Error while updating the Trango SU ID: $error" if $error;
307     }
308
309     warn "[debug]$me Updating svc_broadband with SU ID '$new_suid'...\n" .
310       &Dumper($new_svc) if $DEBUG;
311
312     $error = eval { $new_svc->replace($old_svc); };
313
314     if ($@ or $error) {
315       $error ||= $@;
316       $dbh->rollback if $oldAutoCommit;
317       return "Error while updating the Trango SU ID: $error" if $error;
318     }
319
320     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
321
322   }
323
324   return '';
325
326 }
327
328 sub _check_mac {
329
330   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
331   my $old = shift if $action eq 'replace';
332
333   my $mac_field = $self->option('mac_field');
334   unless (grep {$_ eq $mac_field} $svc_broadband->fields) {
335     return "Missing Trango MAC address field.  "
336       . "See the trango export options for more info.";
337   }
338
339   my $mac_addr = $svc_broadband->getfield($mac_field);
340   unless (length(join('', $mac_addr =~ /[0-9a-fA-F]/g)) == 12) {
341     return "Invalid Trango MAC address: $mac_addr";
342   }
343
344   return('');
345
346 }
347
348 sub _get_next_suid {
349
350   my ($self, $router) = (shift, shift);
351
352   my $counter_dir = '/usr/local/etc/freeside/export.'. datasrc . '/trango';
353   my $baseid = $router->getfield('trango_baseid');
354   my $apid = $router->getfield('trango_apid');
355
356   my $counter_file_suffix = '';
357   if ($baseid ne '') {
358     $counter_file_suffix .= "_B$baseid";
359     if ($apid ne '') {
360       $counter_file_suffix .= "_A$apid";
361     }
362   }
363
364   my $counter_file = $counter_dir . '/SUID' . $counter_file_suffix;
365
366   warn "[debug]$me Using SUID counter file '$counter_file'";
367
368   my $suid = eval {
369     mkdir $counter_dir, 0700 unless -d $counter_dir;
370
371     my $cf = new File::CounterFile($counter_file, 0);
372     $cf->inc;
373   };
374
375   die "Error generating next Trango SU ID: $@" if (not $suid or $@);
376
377   return($suid);
378
379 }
380
381
382
383 # Trango-specific subroutines for generating varbind values.
384 #
385 # All subs should die on error, and return undef to decline.  OIDs that
386 # decline will not be added to varbinds.
387
388 sub _trango_access5830_sudbDeleteOrAddId {
389
390   my ($self, $action, $router) = (shift, shift, shift);
391   my ($svc_broadband) = shift;
392   my $old = shift if $action eq 'replace';
393
394   my $suid = $svc_broadband->getfield($self->option('suid_field'));
395
396   # Sanity check.
397   unless ($suid =~ /^\d+$/) {
398     if ($action eq 'delete') {
399       # Silently ignore.  If we don't have a valid SU ID now, we probably
400       # never did.
401       return undef;
402     } else {
403       die "Invalid Trango SU ID '$suid'";
404     }
405   }
406
407   return ($suid);
408
409 }
410
411 sub _trango_access5830_sudbAddMac {
412
413   my ($self, $action, $router) = (shift, shift, shift);
414   my ($svc_broadband) = shift;
415   my $old = shift if $action eq 'replace';
416
417   my $mac_addr = $svc_broadband->getfield($self->option('mac_field'));
418   $mac_addr = join('', $mac_addr =~ /[0-9a-fA-F]/g);
419
420   # Sanity check.
421   die "Invalid Trango MAC address '$mac_addr'" unless (length($mac_addr)==12);
422
423   return($mac_addr);
424
425 }
426
427
428 =head1 BUGS
429
430 Plenty, I'm sure.
431
432 =cut
433
434
435 1;