summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkhoff <khoff>2007-01-31 05:43:52 +0000
committerkhoff <khoff>2007-01-31 05:43:52 +0000
commitd11482aaa34f11b5007741e2099af46750805c11 (patch)
tree76530f309e193ec3c3bd70e50e7bf81411eb0c6a
parentc4f427df48d37522ac5822bd9eca5e9151044e19 (diff)
FS::part_export::router
- Refactored to be more easily sub-classed. - Moved per-export options to FS:;router virtual fields. - Fixed other general brokenness. FS::part_export::snmp - SNMP export sub-classed from FS::part_export::router FS::part_export::trango - Export for Trango proprietary access points. Sub-classed from FS::part_export::snmp.
-rw-r--r--FS/FS/part_export/router.pm248
-rw-r--r--FS/FS/part_export/snmp.pm255
-rw-r--r--FS/FS/part_export/trango.pm434
3 files changed, 856 insertions, 81 deletions
diff --git a/FS/FS/part_export/router.pm b/FS/FS/part_export/router.pm
index 648a4372b..e14b57932 100644
--- a/FS/FS/part_export/router.pm
+++ b/FS/FS/part_export/router.pm
@@ -5,35 +5,47 @@ package FS::part_export::router;
This export connects to a router and transmits commands via telnet or SSH.
It requires the following custom router fields:
+=head1 Required custom fields
+
=over 4
-=item admin_address - IP address (or hostname) to connect
+=item admin_address - IP address (or hostname) to connect.
-=item admin_user - username for admin access
+=item admin_user - Username for the router.
-=item admin_password - password for admin access
+=item admin_password - Password for the router.
-=back
+=item admin_protocol - Protocol to use for the router. 'telnet' or 'ssh'. The ssh protocol only support password-less (ie. RSA key) authentication. As such, the admin_password field isn't used if ssh is specified.
-The export itself needs the following options:
+=item admin_timeout - Time in seconds to wait for a connection.
-=over 4
+=item admin_prompt - A regular expression matching the router's prompt. See Net::Telnet for details. Only applies to the 'telnet' protocol.
+
+=item admin_cmd_insert - Insert export command. See below.
-=item insert, replace, delete - command strings (to be interpolated)
+=item admin_cmd_delete - Delete export command. See below.
-=item Prompt - prompt string to expect from router after successful login
+=item admin_cmd_replace - Replace export command. See below.
-=item Timeout - time to wait for prompt string
+=item admin_cmd_suspend - Suspend export command. See below.
+
+=item admin_cmd_unsuspend - Unsuspend export command. See below.
+
+The admin_cmd_* virtual fields, if set, will be double quoted, eval'd, and executed on the router specified.
+
+If any of the required router virtual fields are not defined, then the export silently declines.
=back
-(Prompt and Timeout are required only for telnet connections.)
+The export itself takes no options.
=cut
-use vars qw(@ISA %info @saltset);
+use strict;
+use vars qw(@ISA %info $me $DEBUG);
use Tie::IxHash;
use String::ShellQuote;
+use FS::Record qw(qsearchs);
use FS::part_export;
@ISA = qw(FS::part_export);
@@ -44,26 +56,32 @@ tie my %options, 'Tie::IxHash',
type =>'select',
options => [qw(telnet ssh)],
default => 'telnet'},
- 'insert' => {label=>'Insert command', default=>'' },
- 'delete' => {label=>'Delete command', default=>'' },
- 'replace' => {label=>'Replace command', default=>'' },
- 'Timeout' => {label=>'Time to wait for prompt', default=>'20' },
- 'Prompt' => {label=>'Prompt string', default=>'#' }
;
%info = (
'svc' => 'svc_broadband',
'desc' => 'Send a command to a router.',
'options' => \%options,
- 'notes' => 'Installation of Net::Telnet from CPAN is required for telnet connections. ( more detailed description from Kristian / fire2wire? )',
+ 'notes' => 'Installation of Net::Telnet from CPAN is required for telnet connections. This export will execute if the following virtual fields are set on the router: admin_user, admin_password, admin_address, admin_timeout, admin_prompt. Option virtual fields are: admin_cmd_insert, admin_cmd_replace, admin_cmd_delete, admin_cmd_suspend, admin_cmd_unsuspend.',
);
-@saltset = ( 'a'..'z' , 'A'..'Z' , '0'..'9' , '.' , '/' );
+$me = '[' . __PACKAGE__ . ']';
+$DEBUG = 1;
+
sub rebless { shift; }
+sub _field_prefix { 'admin'; }
+
+sub _req_router_fields {
+ map {
+ $_[0]->_field_prefix . '_' . $_
+ } (qw(address prompt user));
+}
+
sub _export_insert {
my($self) = shift;
+ warn "Running insert for " . ref($self);
$self->_export_command('insert', @_);
}
@@ -82,83 +100,159 @@ sub _export_unsuspend {
$self->_export_command('unsuspend', @_);
}
-sub _export_command {
- my ( $self, $action, $svc_broadband) = (shift, shift, shift);
- my $command = $self->option($action);
- return '' if $command =~ /^\s*$/;
+sub _export_replace {
+ my($self) = shift;
+ $self->_export_command('replace', @_);
+}
- no strict 'vars';
- {
- no strict 'refs';
- ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+sub _export_command {
+ my ($self, $action, $svc_broadband) = (shift, shift, shift);
+ my ($error, $old);
+
+ if ($action eq 'replace') {
+ $old = shift;
}
+
+ warn "[debug]$me Processing action '$action'" if $DEBUG;
+
# fetch router info
- my $router = $svc_broadband->addr_block->router;
- my %r;
- $r{$_} = $router->getfield($_) foreach $router->virtual_fields;
- #warn qq("$command");
- #warn eval(qq("$command"));
-
- warn "admin_address: '$r{admin_address}'";
-
- if ($r{admin_address} ne '') {
- $self->router_queue( $svc_broadband->svcnum, $self->option('protocol'),
- user => $r{admin_user},
- password => $r{admin_password},
- host => $r{admin_address},
- Timeout => $self->option('Timeout'),
- Prompt => $self->option('Prompt'),
- command => eval(qq("$command")),
- );
- } else {
+ my $router = $self->_get_router($svc_broadband, @_);
+ unless ($router) {
+ return "Unable to lookup router for $action export";
+ }
+
+ unless ($self->_check_router_fields($router)) {
+ # Virtual fields aren't defined. Exit silently.
+ warn "[debug]$me Required router virtual fields not defined. Returning...";
return '';
}
+
+ my $args;
+ ($error, $args) = $self->_prepare_args(
+ $action,
+ $router,
+ $svc_broadband,
+ ($old ? $old : ()),
+ @_
+ );
+
+ if ($error) {
+ # Error occured while preparing args.
+ return $error;
+ } elsif (not defined $args) {
+ # Silently decline.
+ warn "[debug]$me Declining '$action' export";
+ return '';
+ } # else ... queue the export.
+
+ warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
+
+ return(
+ $self->_queue(
+ $svc_broadband->svcnum,
+ $self->_get_cmd_sub($svc_broadband, $router),
+ @$args
+ )
+ );
+
}
-sub _export_replace {
+sub _prepare_args {
- # We don't handle the case of a svc_broadband moving between routers.
- # If you want to do that, reprovision the service.
+ my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
+ my $old = shift if ($action eq 'replace');
+
+ my $field_prefix = $self->_field_prefix;
+ my $command = $router->getfield("${field_prefix}_cmd_${action}");
+ unless ($command) {
+ warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
+ . "is not defined." if $DEBUG;
+ return '';
+ }
- my($self, $new, $old ) = (shift, shift, shift);
- my $command = $self->option('replace');
- no strict 'vars';
{
+ no strict 'vars';
no strict 'refs';
- ${"old_$_"} = $old->getfield($_) foreach $old->fields;
- ${"new_$_"} = $new->getfield($_) foreach $new->fields;
+
+ if ($action eq 'replace') {
+ ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+ ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+ $command = eval(qq("$command"));
+ } else {
+ ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+ $command = eval(qq("$command"));
+ }
+ return $@ if $@;
}
- my $router = $new->addr_block->router;
- my %r;
- $r{$_} = $router->getfield($_) foreach $router->virtual_fields;
-
- if ($r{admin_address} ne '') {
- $self->router_queue( $new->svcnum, $self->option('protocol'),
- user => $r{admin_user},
- password => $r{admin_password},
- host => $r{admin_address},
- Timeout => $self->option('Timeout'),
- Prompt => $self->option('Prompt'),
- command => eval(qq("$command")),
- );
- } else {
- return '';
+ my $args = [
+ 'user' => $router->getfield($field_prefix . '_user'),
+ 'password' => $router->getfield($field_prefix . '_password'),
+ 'host' => $router->getfield($field_prefix . '_address'),
+ 'Timeout' => $router->getfield($field_prefix . '_timeout'),
+ 'Prompt' => $router->getfield($field_prefix . '_prompt'),
+ 'command' => $command,
+ ];
+
+ return('', $args);
+
+}
+
+sub _get_cmd_sub {
+
+ my ($self, $svc_broadband, $router) = (shift, shift, shift);
+
+ my $protocol = (
+ $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
+ ) ? $1 : 'telnet';
+
+ return(ref($self)."::".$protocol."_cmd");
+
+}
+
+sub _check_router_fields {
+
+ my ($self, $router, $action) = (shift, shift, shift);
+ my @check_fields = $self->_req_router_fields;
+
+ foreach (@check_fields) {
+ if ($router->getfield($_) eq '') {
+ warn "[debug]$me Required field '$_' is unset";
+ return 0;
+ } else {
+ return 1;
+ }
}
+
}
-#a good idea to queue anything that could fail or take any time
-sub router_queue {
+sub _queue {
#warn join ':', @_;
- my( $self, $svcnum, $protocol ) = (shift, shift, shift);
+ my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
my $queue = new FS::queue {
'svcnum' => $svcnum,
};
- $queue->job ("FS::part_export::router::".$protocol."_cmd");
- $queue->insert( @_ );
+ $queue->job($cmd_sub);
+ $queue->insert(@_);
+}
+
+sub _get_router {
+ my ($self, $svc_broadband, %args) = (shift, shift, shift, @_);
+
+ my $router;
+ if ($args{'routernum'}) {
+ $router = qsearchs('router', { routernum => $args{'routernum'}});
+ } else {
+ $router = $svc_broadband->addr_block->router;
+ }
+
+ return($router);
+
}
-sub ssh_cmd { #subroutine, not method
+
+# Subroutines
+sub ssh_cmd {
use Net::SSH '0.08';
&Net::SSH::ssh_cmd( { @_ } );
}
@@ -179,12 +273,4 @@ sub telnet_cmd {
die @error if (grep /^ERROR/, @error);
}
-#sub router_insert { #subroutine, not method
-#}
-#sub router_replace { #subroutine, not method
-#}
-#sub router_delete { #subroutine, not method
-#}
-
1;
-
diff --git a/FS/FS/part_export/snmp.pm b/FS/FS/part_export/snmp.pm
new file mode 100644
index 000000000..14781e0b0
--- /dev/null
+++ b/FS/FS/part_export/snmp.pm
@@ -0,0 +1,255 @@
+package FS::part_export::snmp;
+
+=head1 FS::part_export::snmp
+
+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.
+
+=head1 Required custom fields
+
+=over 4
+
+=item snmp_address - IP address (or hostname) of the router/agent
+
+=item snmp_comm - R/W SNMP community of the router/agent
+
+=item snmp_version - SNMP version of the router/agent
+
+=back
+
+=head1 Optional custom fields
+
+=over 4
+
+=item snmp_cmd_insert - SNMP SETs to perform on insert. See L</Formatting>
+
+=item snmp_cmd_replace - SNMP SETs to perform on replace. See L</Formatting>
+
+=item snmp_cmd_delete - SNMP SETs to perform on delete. See L</Formatting>
+
+=item snmp_cmd_suspend - SNMP SETs to perform on suspend. See L</Formatting>
+
+=item snmp_cmd_unsuspend - SNMP SETs to perform on unsuspend. See L</Formatting>
+
+=back
+
+=head1 Formatting
+
+The values for the snmp_cmd_* fields should be formatted as follows:
+
+<OID>|<Data Type>|<expr>[||<OID>|<Data Type>|<expr>[...]]
+
+=over 4
+
+=item OID - SNMP object ID (ex. 1.3.6.1.4.1.1.20). If the OID string starts with a '.', then the Private Enterprise OID (1.3.6.1.4.1) is prepended.
+
+=item Data Type - SNMP data types understood by L<Net::SNMP>, as well as HEX_STRING for convenience. ex. INTEGER, OCTET_STRING, IPADDRESS, ...
+
+=item expr - Expression to be eval'd by freeside. By default, the expression is double quoted and eval'd with all FS::svc_broadband fields available as scalars (ex. $svcnum, $ip_addr, $speed_up). However, if the expression contains a non-escaped double quote, the expression is eval'd without being double quoted. In this case, the expression must be a block of valid perl code that returns the desired value.
+
+You must escape non-delimiter pipes ("|") with a backslash.
+
+=back
+
+=head1 Examples
+
+This is an example for exporting to a Trango Access5830 AP. Newlines inserted for clarity.
+
+=over 4
+
+=item snmp_cmd_delete -
+
+1.3.6.1.4.1.5454.1.20.3.5.1|INTEGER|50||
+1.3.6.1.4.1.5454.1.20.3.5.8|INTEGER|1|
+
+=item snmp_cmd_insert -
+
+1.3.6.1.4.1.5454.1.20.3.5.1|INTEGER|50||
+1.3.6.1.4.1.5454.1.20.3.5.2|HEX_STRING|join("",$radio_addr =~ /[0-9a-fA-F]{2}/g)||
+1.3.6.1.4.1.5454.1.20.3.5.7|INTEGER|1|
+
+=item snmp_cmd_replace -
+
+1.3.6.1.4.1.5454.1.20.3.5.1|INTEGER|50||
+1.3.6.1.4.1.5454.1.20.3.5.8|INTEGER|1||1.3.6.1.4.1.5454.1.20.3.5.1|INTEGER|50||
+1.3.6.1.4.1.5454.1.20.3.5.2|HEX_STRING|join("",$new_radio_addr =~ /[0-9a-fA-F]{2}/g)||
+1.3.6.1.4.1.5454.1.20.3.5.7|INTEGER|1|
+
+=back
+
+=cut
+
+
+use strict;
+use vars qw(@ISA %info $me $DEBUG);
+use Tie::IxHash;
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export;
+use FS::part_export::router;
+
+@ISA = qw(FS::part_export::router);
+
+tie my %options, 'Tie::IxHash', ();
+
+%info = (
+ 'svc' => 'svc_broadband',
+ 'desc' => 'Sends SNMP SETs to an SNMP agent.',
+ 'options' => \%options,
+ 'notes' => 'Requires Net::SNMP. See the documentation for FS::part_export::snmp for required virtual fields and usage information.',
+);
+
+$me= '[' . __PACKAGE__ . ']';
+$DEBUG = 1;
+
+
+sub _field_prefix { 'snmp'; }
+
+sub _req_router_fields {
+ map {
+ $_[0]->_field_prefix . '_' . $_
+ } (qw(address comm version));
+}
+
+sub _get_cmd_sub {
+
+ my ($self, $svc_broadband, $router) = (shift, shift, shift);
+
+ return(ref($self) . '::snmp_cmd');
+
+}
+
+sub _prepare_args {
+
+ my ($self, $action, $router) = (shift, shift, shift);
+ my ($svc_broadband) = shift;
+ my $old;
+ my $field_prefix = $self->_field_prefix;
+
+ if ($action eq 'replace') { $old = shift; }
+
+ my $raw_cmd = $router->getfield("${field_prefix}_cmd_${action}");
+ unless ($raw_cmd) {
+ warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
+ . "is not defined." if $DEBUG;
+ return '';
+ }
+
+ my $args = [
+ '-hostname' => $router->getfield($field_prefix.'_address'),
+ '-version' => $router->getfield($field_prefix.'_version'),
+ '-community' => $router->getfield($field_prefix.'_comm'),
+ ];
+
+ my @varbindlist = ();
+
+ foreach my $snmp_cmd ($raw_cmd =~ m/(.*?[^\\])(?:\|\||$)/g) {
+
+ warn "[debug]$me snmp_cmd is '$snmp_cmd'" if $DEBUG;
+
+ my ($oid, $type, $expr) = $snmp_cmd =~ m/(.*?[^\\])(?:\||$)/g;
+
+ if ($oid =~ /^([\d\.]+)$/) {
+ $oid = $1;
+ $oid = ($oid =~ /^\./) ? '1.3.6.1.4.1' . $oid : $oid;
+ } else {
+ return "Invalid SNMP OID '$oid'";
+ }
+
+ if ($type =~ /^([A-Z_\d]+)$/) {
+ $type = $1;
+ } else {
+ return "Invalid SNMP ASN.1 type '$type'";
+ }
+
+ if ($expr =~ /^(.*)$/) {
+ $expr = $1;
+ } else {
+ return "Invalid expression '$expr'";
+ }
+
+ {
+ no strict 'vars';
+ no strict 'refs';
+
+ if ($action eq 'replace') {
+ ${"old_$_"} = $old->getfield($_) foreach $old->fields;
+ ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+ $expr = ($expr =~/[^\\]"/) ? eval($expr) : eval(qq("$expr"));
+ } else {
+ ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
+ $expr = ($expr =~/[^\\]"/) ? eval($expr) : eval(qq("$expr"));
+ }
+ return $@ if $@;
+ }
+
+ push @varbindlist, ($oid, $type, $expr);
+
+ }
+
+ push @$args, ('-varbindlist', @varbindlist);
+
+ return('', $args);
+
+}
+
+sub snmp_cmd {
+ require Net::SNMP;
+
+ my %args = ();
+ my @varbindlist = ();
+ while (scalar(@_)) {
+ my $key = shift;
+ if ($key eq '-varbindlist') {
+ push @varbindlist, @_;
+ last;
+ } else {
+ $args{$key} = shift;
+ }
+ }
+
+ my $i = 0;
+ while ($i*3 < scalar(@varbindlist)) {
+ my $type_index = ($i*3)+1;
+ my $type_name = $varbindlist[$type_index];
+
+ # Implementing HEX_STRING outselves since Net::SNMP doesn't. Ewwww!
+ if ($type_name eq 'HEX_STRING') {
+ my $value_index = $type_index + 1;
+ $type_name = 'OCTET_STRING';
+ $varbindlist[$value_index] = pack('H*', $varbindlist[$value_index]);
+ }
+
+ my $type = eval "Net::SNMP::$type_name";
+ if ($@ or not defined $type) {
+ warn $@ if $DEBUG;
+ die "snmp_cmd error: Unable to lookup type '$type_name'";
+ }
+
+ $varbindlist[$type_index] = $type;
+ } continue {
+ $i++;
+ }
+
+ my ($snmp, $error) = Net::SNMP->session(%args);
+ die "snmp_cmd error: $error" unless($snmp);
+
+ my $res = $snmp->set_request('-varbindlist' => \@varbindlist);
+ unless($res) {
+ $error = $snmp->error;
+ $snmp->close;
+ die "snmp_cmd error: " . $error;
+ }
+
+ $snmp->close;
+
+ return '';
+
+}
+
+
+=head1 BUGS
+
+Plenty, I'm sure.
+
+=cut
+
+1;
diff --git a/FS/FS/part_export/trango.pm b/FS/FS/part_export/trango.pm
new file mode 100644
index 000000000..e7f1126dd
--- /dev/null
+++ b/FS/FS/part_export/trango.pm
@@ -0,0 +1,434 @@
+package FS::part_export::trango;
+
+=head1 FS::part_export::trango
+
+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.
+
+=head1 Required custom fields
+
+=over 4
+
+=item trango_address - IP address (or hostname) of the Trango AP.
+
+=item trango_comm - R/W SNMP community of the Trango AP.
+
+=item trango_ap_type - Trango AP Model. Currently 'access5830' is the only supported option.
+
+=back
+
+=head1 Optional custom fields
+
+=over 4
+
+=item trango_baseid - Base ID of the Trango AP. See L</"Generating SU IDs">.
+
+=item trango_apid - AP ID of the Trango AP. See L</"Generating SU IDs">.
+
+=back
+
+=head1 Generating SU IDs
+
+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.
+
+=head1 Device Support
+
+This export has been tested with the Trango Access5830 AP.
+
+
+=cut
+
+
+use strict;
+use vars qw(@ISA %info $me $DEBUG $trango_mib $counter_dir);
+
+use FS::UID qw(dbh datasrc);
+use FS::Record qw(qsearch qsearchs);
+use FS::part_export::snmp;
+
+use Tie::IxHash;
+use File::CounterFile;
+use Data::Dumper qw(Dumper);
+
+@ISA = qw(FS::part_export::snmp);
+
+tie my %options, 'Tie::IxHash', (
+ 'suid_field' => {
+ 'label' => 'Trango SU ID field',
+ 'default' => 'trango_suid',
+ 'notes' => 'Name of the FS::svc_broadband virtual field that will contain the SU ID.',
+ },
+ 'mac_field' => {
+ 'label' => 'Trango MAC address field',
+ 'default' => '',
+ 'notes' => 'Name of the FS::svc_broadband virtual field that will contain the SU\'s MAC address.',
+ },
+);
+
+%info = (
+ 'svc' => 'svc_broadband',
+ 'desc' => 'Sends SNMP SETs to a Trango AP.',
+ 'options' => \%options,
+ 'notes' => 'Requires Net::SNMP. See the documentation for FS::part_export::trango for required virtual fields and usage information.',
+);
+
+$me= '[' . __PACKAGE__ . ']';
+$DEBUG = 1;
+
+$trango_mib = {
+ 'access5830' => {
+ 'snmpversion' => 'snmpv1',
+ 'varbinds' => {
+ 'insert' => [
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbAddMac
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
+ 'type' => 'HEX_STRING',
+ 'value' => \&_trango_access5830_sudbAddMac,
+ },
+ { # sudbAddSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ ],
+ 'delete' => [
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbDeleteSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ ],
+ 'replace' => [
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbDeleteSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbAddMac
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
+ 'type' => 'HEX_STRING',
+ 'value' => \&_trango_access5830_sudbAddMac,
+ },
+ { # sudbAddSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ ],
+ 'suspend' => [
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbDeleteSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.8',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ ],
+ 'unsuspend' => [
+ { # sudbDeleteOrAddID
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.1',
+ 'type' => 'INTEGER',
+ 'value' => \&_trango_access5830_sudbDeleteOrAddId,
+ },
+ { # sudbAddMac
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.2',
+ 'type' => 'HEX_STRING',
+ 'value' => \&_trango_access5830_sudbAddMac,
+ },
+ { # sudbAddSU
+ 'oid' => '1.3.6.1.4.1.5454.1.20.3.5.7',
+ 'type' => 'INTEGER',
+ 'value' => 1,
+ },
+ ],
+ },
+ },
+};
+
+
+sub _field_prefix { 'trango'; }
+
+sub _req_router_fields {
+ map {
+ $_[0]->_field_prefix . '_' . $_
+ } (qw(address comm ap_type suid_field));
+}
+
+sub _get_cmd_sub {
+
+ return('FS::part_export::snmp::snmp_cmd');
+
+}
+
+sub _prepare_args {
+
+ my ($self, $action, $router) = (shift, shift, shift);
+ my ($svc_broadband) = shift;
+ my $old = shift if $action eq 'replace';
+ my $field_prefix = $self->_field_prefix;
+ my $error;
+
+ my $ap_type = $router->getfield($field_prefix . '_ap_type');
+
+ unless (exists $trango_mib->{$ap_type}) {
+ return "Unsupported Trango AP type '$ap_type'";
+ }
+
+ $error = $self->_check_suid(
+ $action, $router, $svc_broadband, ($old) ? $old : ()
+ );
+ return $error if $error;
+
+ $error = $self->_check_mac(
+ $action, $router, $svc_broadband, ($old) ? $old : ()
+ );
+ return $error if $error;
+
+ my $ap_mib = $trango_mib->{$ap_type};
+
+ my $args = [
+ '-hostname' => $router->getfield($field_prefix.'_address'),
+ '-version' => $ap_mib->{'snmpversion'},
+ '-community' => $router->getfield($field_prefix.'_comm'),
+ ];
+
+ my @varbindlist = ();
+
+ foreach my $oid (@{$ap_mib->{'varbinds'}->{$action}}) {
+ warn "[debug]$me Processing OID '" . $oid->{'oid'} . "'" if $DEBUG;
+ my $value;
+ if (ref($oid->{'value'}) eq 'CODE') {
+ eval {
+ $value = &{$oid->{'value'}}(
+ $self, $action, $router, $svc_broadband,
+ (($old) ? $old : ()),
+ );
+ };
+ return "While processing OID '" . $oid->{'oid'} . "':" . $@
+ if $@;
+ } else {
+ $value = $oid->{'value'};
+ }
+
+ warn "[debug]$me Value for OID '" . $oid->{'oid'} . "': " if $DEBUG;
+
+ if (defined $value) { # Skip OIDs with undefined values.
+ push @varbindlist, ($oid->{'oid'}, $oid->{'type'}, $value);
+ }
+ }
+
+
+ push @$args, ('-varbindlist', @varbindlist);
+
+ return('', $args);
+
+}
+
+sub _check_suid {
+
+ my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
+ my $old = shift if $action eq 'replace';
+ my $error;
+
+ my $suid_field = $self->option('suid_field');
+ unless (grep {$_ eq $suid_field} $svc_broadband->fields) {
+ return "Missing Trango SU ID field. "
+ . "See the trango export options for more info.";
+ }
+
+ my $suid = $svc_broadband->getfield($suid_field);
+ if ($action eq 'replace') {
+ my $old_suid = $old->getfield($suid_field);
+
+ if ($old_suid ne '' and $old_suid ne $suid) {
+ return 'Cannot change Trango SU ID';
+ }
+ }
+
+ if (not $suid =~ /^\d+$/ and $action ne 'delete') {
+ my $new_suid = eval { $self->_get_next_suid($router); };
+ return "Error while getting next Trango SU ID: $@" if ($@);
+
+ warn "[debug]$me Got new SU ID: $new_suid" if $DEBUG;
+ $svc_broadband->set($suid_field, $new_suid);
+
+ #FIXME: Probably a bad hack.
+ # We need to update the SU ID field in the database.
+
+ my $oldAutoCommit = $FS::UID::AutoCommit;
+ local $FS::svc_Common::noexport_hack = 1;
+ local $FS::UID::AutoCommit = 0;
+ my $dbh = dbh;
+
+ my $svcnum = $svc_broadband->svcnum;
+
+ my $old_svc = qsearchs('svc_broadband', { svcnum => $svcnum });
+ unless ($old_svc) {
+ return "Unable to retrieve svc_broadband with svcnum '$svcnum";
+ }
+
+ my $svcpart = $svc_broadband->svcpart
+ ? $svc_broadband->svcpart
+ : $svc_broadband->cust_svc->svcpart;
+
+ my $new_svc = new FS::svc_broadband {
+ $old_svc->hash,
+ $suid_field => $new_suid,
+ svcpart => $svcpart,
+ };
+
+ $error = $new_svc->check;
+ if ($error) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error while updating the Trango SU ID: $error" if $error;
+ }
+
+ warn "[debug]$me Updating svc_broadband with SU ID '$new_suid'...\n" .
+ &Dumper($new_svc) if $DEBUG;
+
+ $error = eval { $new_svc->replace($old_svc); };
+
+ if ($@ or $error) {
+ $error ||= $@;
+ $dbh->rollback if $oldAutoCommit;
+ return "Error while updating the Trango SU ID: $error" if $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ }
+
+ return '';
+
+}
+
+sub _check_mac {
+
+ my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
+ my $old = shift if $action eq 'replace';
+
+ my $mac_field = $self->option('mac_field');
+ unless (grep {$_ eq $mac_field} $svc_broadband->fields) {
+ return "Missing Trango MAC address field. "
+ . "See the trango export options for more info.";
+ }
+
+ my $mac_addr = $svc_broadband->getfield($mac_field);
+ unless (length(join('', $mac_addr =~ /[0-9a-fA-F]/g)) == 12) {
+ return "Invalid Trango MAC address: $mac_addr";
+ }
+
+ return('');
+
+}
+
+sub _get_next_suid {
+
+ my ($self, $router) = (shift, shift);
+
+ my $counter_dir = '/usr/local/etc/freeside/export.'. datasrc . '/trango';
+ my $baseid = $router->getfield('trango_baseid');
+ my $apid = $router->getfield('trango_apid');
+
+ my $counter_file_suffix = '';
+ if ($baseid ne '') {
+ $counter_file_suffix .= "_B$baseid";
+ if ($apid ne '') {
+ $counter_file_suffix .= "_A$apid";
+ }
+ }
+
+ my $counter_file = $counter_dir . '/SUID' . $counter_file_suffix;
+
+ warn "[debug]$me Using SUID counter file '$counter_file'";
+
+ my $suid = eval {
+ mkdir $counter_dir, 0700 unless -d $counter_dir;
+
+ my $cf = new File::CounterFile($counter_file, 0);
+ $cf->inc;
+ };
+
+ die "Error generating next Trango SU ID: $@" if (not $suid or $@);
+
+ return($suid);
+
+}
+
+
+
+# Trango-specific subroutines for generating varbind values.
+#
+# All subs should die on error, and return undef to decline. OIDs that
+# decline will not be added to varbinds.
+
+sub _trango_access5830_sudbDeleteOrAddId {
+
+ my ($self, $action, $router) = (shift, shift, shift);
+ my ($svc_broadband) = shift;
+ my $old = shift if $action eq 'replace';
+
+ my $suid = $svc_broadband->getfield($self->option('suid_field'));
+
+ # Sanity check.
+ unless ($suid =~ /^\d+$/) {
+ if ($action eq 'delete') {
+ # Silently ignore. If we don't have a valid SU ID now, we probably
+ # never did.
+ return undef;
+ } else {
+ die "Invalid Trango SU ID '$suid'";
+ }
+ }
+
+ return ($suid);
+
+}
+
+sub _trango_access5830_sudbAddMac {
+
+ my ($self, $action, $router) = (shift, shift, shift);
+ my ($svc_broadband) = shift;
+ my $old = shift if $action eq 'replace';
+
+ my $mac_addr = $svc_broadband->getfield($self->option('mac_field'));
+ $mac_addr = join('', $mac_addr =~ /[0-9a-fA-F]/g);
+
+ # Sanity check.
+ die "Invalid Trango MAC address '$mac_addr'" unless (length($mac_addr)==12);
+
+ return($mac_addr);
+
+}
+
+
+=head1 BUGS
+
+Plenty, I'm sure.
+
+=cut
+
+
+1;