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,
90 '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.',
93 $me = '[' . __PACKAGE__ . ']';
97 sub rebless { shift; }
99 sub _field_prefix { 'admin'; }
101 sub _req_router_fields {
103 $_[0]->_field_prefix . '_' . $_
104 } (qw(address prompt user));
109 warn "Running insert for " . ref($self);
110 $self->_export_command('insert', @_);
115 $self->_export_command('delete', @_);
118 sub _export_suspend {
120 $self->_export_command('suspend', @_);
123 sub _export_unsuspend {
125 $self->_export_command('unsuspend', @_);
128 sub _export_replace {
130 $self->_export_command('replace', @_);
133 sub _export_command {
134 my ($self, $action, $svc_broadband) = (shift, shift, shift);
137 if ($action eq 'replace') {
141 warn "[debug]$me Processing action '$action'" if $DEBUG;
144 my $router = $self->_get_router($svc_broadband, @_);
146 return "Unable to lookup router for $action export";
149 unless ($self->_check_router_fields($router)) {
150 # Virtual fields aren't defined. Exit silently.
151 warn "[debug]$me Required router virtual fields not defined. Returning..."
157 ($error, $args) = $self->_prepare_args(
166 # Error occured while preparing args.
168 } elsif (not defined $args) {
170 warn "[debug]$me Declining '$action' export" if $DEBUG;
172 } # else ... queue the export.
174 warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
178 $svc_broadband->svcnum,
179 $self->_get_cmd_sub($svc_broadband, $router),
188 my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
189 my $old = shift if ($action eq 'replace');
192 my $field_prefix = $self->_field_prefix;
193 my $command = $router->getfield("${field_prefix}_cmd_${action}");
195 warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
196 . "is not defined." if $DEBUG;
200 if ($command =~ /\[\@--/) { # Use Text::Template
202 my $template_data = {};
204 if ($action eq 'replace') {
205 $template_data->{"old_$_"} = $old->getfield($_) foreach $old->fields;
206 $template_data->{"new_$_"} = $svc_broadband->getfield($_)
207 foreach $svc_broadband->fields;
209 $template_data->{$_} = $svc_broadband->getfield($_)
210 foreach $svc_broadband->fields;
213 my $template = new Text::Template (
216 DELIMITERS => [ '[@--', '--@]' ],
217 ) or return "Unable to construct template for router command: "
218 . $Text::Template::ERROR;
220 $command = $template->fill_in(
221 HASH => $template_data,
222 BROKEN_ARG => \$error,
225 my $err = $bargs{'arg'};
226 $$err = $bargs{'error'};
231 if (not defined $command or $error) {
232 $error ||= $Text::Template::ERROR;
233 return "Unable to fill-in template for router command: $error";
240 if ($action eq 'replace') {
241 ${"old_$_"} = $old->getfield($_) foreach $old->fields;
242 ${"new_$_"} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
243 $command = eval(qq("$command"));
245 ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
246 $command = eval(qq("$command"));
252 'user' => $router->getfield($field_prefix . '_user'),
253 'password' => $router->getfield($field_prefix . '_password'),
254 'host' => $router->getfield($field_prefix . '_address'),
255 'Timeout' => $router->getfield($field_prefix . '_timeout'),
256 'Prompt' => $router->getfield($field_prefix . '_prompt'),
257 'command' => $command,
260 my $error_check = $router->getfield("${field_prefix}_cmd_${action}_error");
261 push(@$args, ('error_check' => $error_check)) if ($error_check);
269 my ($self, $svc_broadband, $router) = (shift, shift, shift);
272 $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
275 return(ref($self)."::".$protocol."_cmd");
279 sub _check_router_fields {
281 my ($self, $router, $action) = (shift, shift, shift);
282 my @check_fields = $self->_req_router_fields;
284 foreach (@check_fields) {
285 if ($router->getfield($_) eq '') {
286 warn "[debug]$me Required field '$_' is unset" if $DEBUG;
296 my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
297 my $queue = new FS::queue {
300 $queue->job($cmd_sub);
305 my ($self, $svc_broadband, %args) = (shift, shift, @_);
308 if ($args{'routernum'}) {
309 $router = qsearchs('router', { routernum => $args{'routernum'}});
311 $router = $svc_broadband->router;
323 eval 'use Net::SSH \'0.08\'';
326 my @out = &Net::SSH::ssh_cmd( { @_ } );
327 my $error = &_cmd_error_check(\%arg, \@out);
329 die ("Error while processing ssh command: $error") if $error;
338 eval 'use Net::Telnet';
341 my $t = new Net::Telnet (Timeout => $arg{'Timeout'},
342 Prompt => $arg{'Prompt'});
343 $t->open($arg{'host'});
344 $t->login($arg{'user'}, $arg{'password'});
345 my @out = $t->cmd($arg{'command'});
346 my $error = &_cmd_error_check(\%arg, \@out);
348 die ("Error while processing telnet command: $error") if $error;
354 sub _cmd_error_check {
355 my ($arg, $out) = (shift, shift);
357 die "_cmd_error_check called without proper arguments"
358 unless (ref($arg) eq 'HASH' and ref($out) eq 'ARRAY');
360 unless (exists($arg->{'error_check'}) and $arg->{'error_check'} ne '') {
361 #Preserve default behaviour and return output if a check isn't defined.
362 warn "Output from router command: " . join('', @$out) if $DEBUG;
366 my $error_check = $arg->{'error_check'};
368 return $_ if /$error_check/;