1 package FS::part_export::router;
3 =head1 FS::part_export::router
5 This export connects to a router and transmits commands via telnet or SSH.
6 It requires the following custom router fields:
8 =head1 Required custom fields
12 =item admin_address - IP address (or hostname) to connect.
14 =item admin_user - Username for the router.
16 =item admin_password - Password for the router.
18 =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.
20 =item admin_timeout - Time in seconds to wait for a connection.
22 =item admin_prompt - A regular expression matching the router's prompt. See Net::Telnet for details. Only applies to the 'telnet' protocol.
24 =item admin_cmd_insert - Insert export command.
26 =item admin_cmd_insert_error - Insert export command error pattern.
28 =item admin_cmd_delete - Delete export command.
30 =item admin_cmd_delete_error - Delete export command error pattern.
32 =item admin_cmd_replace - Replace export command.
34 =item admin_cmd_replace_error - Replace export command error pattern.
36 =item admin_cmd_suspend - Suspend export command.
38 =item admin_cmd_suspend_error - Support export command error pattern.
40 =item admin_cmd_unsuspend - Unsuspend export command.
42 =item admin_cmd_unsuspend_error - Unsuspend export command error pattern.
44 The admin_cmd_* virtual fields, if set, will be processed in one of two ways. After being expanded, they will be run on the router specified by admin_address using the protocol specified by admin_protocol.
50 If the export command contains the string [@--, then it will be processed with Text::Template using [@-- and --@] as delimeters.
54 If the export command does not contain [@--, it will be double quoted and eval'd.
58 The admin_cmd_*_error virtual fields, if set, define a regular expression that will be matched against the output of the command being run. If the pattern matches, an error will be raised using the output as the error.
60 If any of the required router virtual fields are not defined, then the export silently declines.
64 The export itself takes no options.
69 use vars qw(@ISA %info $me $DEBUG);
73 use FS::Record qw(qsearchs);
76 @ISA = qw(FS::part_export);
78 tie my %options, 'Tie::IxHash',
82 options => [qw(telnet ssh)],
87 'svc' => 'svc_broadband',
88 'desc' => 'Send a command to a router.',
89 'options' => \%options,
91 '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. See the module documentation for a full list of required/supported router virtual fields.',
94 $me = '[' . __PACKAGE__ . ']';
98 sub rebless { shift; }
100 sub _field_prefix { 'admin'; }
102 sub _req_router_fields {
104 $_[0]->_field_prefix . '_' . $_
105 } (qw(address prompt user));
110 warn "Running insert for " . ref($self);
111 $self->_export_command('insert', @_);
116 $self->_export_command('delete', @_);
119 sub _export_suspend {
121 $self->_export_command('suspend', @_);
124 sub _export_unsuspend {
126 $self->_export_command('unsuspend', @_);
129 sub _export_replace {
131 $self->_export_command('replace', @_);
134 sub _export_command {
135 my ($self, $action, $svc_broadband) = (shift, shift, shift);
138 if ($action eq 'replace') {
142 warn "[debug]$me Processing action '$action'" if $DEBUG;
145 my $router = $self->_get_router($svc_broadband, @_);
147 return "Unable to lookup router for $action export";
150 unless ($self->_check_router_fields($router)) {
151 # Virtual fields aren't defined. Exit silently.
152 warn "[debug]$me Required router virtual fields not defined. Returning..."
158 ($error, $args) = $self->_prepare_args(
167 # Error occured while preparing args.
169 } elsif (not defined $args) {
171 warn "[debug]$me Declining '$action' export" if $DEBUG;
173 } # else ... queue the export.
175 warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
179 $svc_broadband->svcnum,
180 $self->_get_cmd_sub($svc_broadband, $router),
189 my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
190 my $old = shift if ($action eq 'replace');
193 my $field_prefix = $self->_field_prefix;
194 my $command = $router->getfield("${field_prefix}_cmd_${action}");
196 warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
197 . "is not defined." if $DEBUG;
201 if ($command =~ /\[\@--/) { # Use Text::Template
203 my $template_data = {};
205 if ($action eq 'replace') {
206 $template_data->{"old_$_"} = $old->getfield($_) foreach $old->fields;
207 $template_data->{"new_$_"} = $svc_broadband->getfield($_)
208 foreach $svc_broadband->fields;
210 $template_data->{$_} = $svc_broadband->getfield($_)
211 foreach $svc_broadband->fields;
214 my $template = new Text::Template (
217 DELIMITERS => [ '[@--', '--@]' ],
218 ) or return "Unable to construct template for router command: "
219 . $Text::Template::ERROR;
221 $command = $template->fill_in(
222 HASH => $template_data,
223 BROKEN_ARG => \$error,
226 my $err = $bargs{'arg'};
227 $$err = $bargs{'error'};
232 if (not defined $command or $error) {
233 $error ||= $Text::Template::ERROR;
234 return "Unable to fill-in template for router command: $error";
241 if ($action eq 'replace') {
242 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
243 ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
244 $command = eval(qq("$command"));
246 ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
247 $command = eval(qq("$command"));
253 'user' => $router->getfield($field_prefix . '_user'),
254 'password' => $router->getfield($field_prefix . '_password'),
255 'host' => $router->getfield($field_prefix . '_address'),
256 'Timeout' => $router->getfield($field_prefix . '_timeout'),
257 'Prompt' => $router->getfield($field_prefix . '_prompt'),
258 'command' => $command,
261 my $error_check = $router->getfield("${field_prefix}_cmd_${action}_error");
262 push(@$args, ('error_check' => $error_check)) if ($error_check);
270 my ($self, $svc_broadband, $router) = (shift, shift, shift);
273 $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
276 return(ref($self)."::".$protocol."_cmd");
280 sub _check_router_fields {
282 my ($self, $router, $action) = (shift, shift, shift);
283 my @check_fields = $self->_req_router_fields;
285 foreach (@check_fields) {
286 if ($router->getfield($_) eq '') {
287 warn "[debug]$me Required field '$_' is unset" if $DEBUG;
297 my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
298 my $queue = new FS::queue {
301 $queue->job($cmd_sub);
306 my ($self, $svc_broadband, %args) = (shift, shift, @_);
309 if ($args{'routernum'}) {
310 $router = qsearchs('router', { routernum => $args{'routernum'}});
312 $router = $svc_broadband->router;
324 eval 'use Net::SSH \'0.08\'';
327 my @out = &Net::SSH::ssh_cmd( { @_ } );
328 my $error = &_cmd_error_check(\%arg, \@out);
330 die ("Error while processing ssh command: $error") if $error;
339 eval 'use Net::Telnet';
342 my $t = new Net::Telnet (Timeout => $arg{'Timeout'},
343 Prompt => $arg{'Prompt'});
344 $t->open($arg{'host'});
345 $t->login($arg{'user'}, $arg{'password'});
346 my @out = $t->cmd($arg{'command'});
347 my $error = &_cmd_error_check(\%arg, \@out);
349 die ("Error while processing telnet command: $error") if $error;
355 sub _cmd_error_check {
356 my ($arg, $out) = (shift, shift);
358 die "_cmd_error_check called without proper arguments"
359 unless (ref($arg) eq 'HASH' and ref($out) eq 'ARRAY');
361 unless (exists($arg->{'error_check'}) and $arg->{'error_check'} ne '') {
362 #Preserve default behaviour and return output if a check isn't defined.
363 warn "Output from router command: " . join('', @$out) if $DEBUG;
367 my $error_check = $arg->{'error_check'};
369 return $_ if /$error_check/;