diff options
| author | khoff <khoff> | 2007-01-31 05:43:52 +0000 | 
|---|---|---|
| committer | khoff <khoff> | 2007-01-31 05:43:52 +0000 | 
| commit | d11482aaa34f11b5007741e2099af46750805c11 (patch) | |
| tree | 76530f309e193ec3c3bd70e50e7bf81411eb0c6a /FS | |
| parent | c4f427df48d37522ac5822bd9eca5e9151044e19 (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.
Diffstat (limited to 'FS')
| -rw-r--r-- | FS/FS/part_export/router.pm | 248 | ||||
| -rw-r--r-- | FS/FS/part_export/snmp.pm | 255 | ||||
| -rw-r--r-- | FS/FS/part_export/trango.pm | 434 | 
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; | 
