[freeside-commits] freeside/FS/FS/part_export router.pm, 1.4, 1.5 snmp.pm, NONE, 1.1 trango.pm, NONE, 1.1

Kristian Hoffmann,420,, khoff at wavetail.420.am
Tue Jan 30 21:43:55 PST 2007


Update of /home/cvs/cvsroot/freeside/FS/FS/part_export
In directory wavetail:/tmp/cvs-serv27541

Modified Files:
	router.pm 
Added Files:
	snmp.pm trango.pm 
Log Message:
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.


--- NEW FILE: trango.pm ---
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;

--- NEW FILE: snmp.pm ---
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;

Index: router.pm
===================================================================
RCS file: /home/cvs/cvsroot/freeside/FS/FS/part_export/router.pm,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -d -r1.4 -r1.5
--- router.pm	26 Mar 2004 04:46:54 -0000	1.4
+++ router.pm	31 Jan 2007 05:43:52 -0000	1.5
@@ -5,35 +5,47 @@
 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 insert, replace, delete - command strings (to be interpolated)
+=item admin_cmd_insert - Insert export command.  See below.
 
-=item Prompt - prompt string to expect from router after successful login
+=item admin_cmd_delete - Delete export command.  See below.
 
-=item Timeout - time to wait for prompt string
+=item admin_cmd_replace - Replace export command.  See below.
+
+=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 @@
 	  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.',
 );
 
- at 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 @@
   $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;
   }
-  # 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}'";
+ warn "[debug]$me Processing action '$action'" if $DEBUG;
 
-  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 {
+  # fetch router info
+  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;
+  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,
+  ];
 
-  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 '';
+  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 ssh_cmd { #subroutine, not method
+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);
+
+}
+
+
+# Subroutines
+sub ssh_cmd {
   use Net::SSH '0.08';
   &Net::SSH::ssh_cmd( { @_ } );
 }
@@ -179,12 +273,4 @@
   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;
-



More information about the freeside-commits mailing list