Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / FS / FS / part_export / router.pm
1 package FS::part_export::router;
2
3 =head1 FS::part_export::router
4
5 This export connects to a router and transmits commands via telnet or SSH.
6 It requires the following custom router fields:
7
8 =head1 Required custom fields
9
10 =over 4
11
12 =item admin_address - IP address (or hostname) to connect.
13
14 =item admin_user - Username for the router.
15
16 =item admin_password - Password for the  router.
17
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.
19
20 =item admin_timeout - Time in seconds to wait for a connection.
21
22 =item admin_prompt - A regular expression matching the router's prompt.  See Net::Telnet for details.  Only applies to the 'telnet' protocol.
23
24 =item admin_cmd_insert - Insert export command.
25
26 =item admin_cmd_insert_error - Insert export command error pattern.
27
28 =item admin_cmd_delete - Delete export command.
29
30 =item admin_cmd_delete_error - Delete export command error pattern.
31
32 =item admin_cmd_replace - Replace export command.
33
34 =item admin_cmd_replace_error - Replace export command error pattern.
35
36 =item admin_cmd_suspend - Suspend export command.
37
38 =item admin_cmd_suspend_error - Support export command error pattern.
39
40 =item admin_cmd_unsuspend - Unsuspend export command.
41
42 =item admin_cmd_unsuspend_error - Unsuspend export command error pattern.
43
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.
45
46 =over 4
47
48 =item Text::Template
49
50 If the export command contains the string [@--, then it will be processed with Text::Template using [@-- and --@] as delimeters.
51
52 =item eval
53
54 If the export command does not contain [@--, it will be double quoted and eval'd.
55
56 =back
57
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.
59
60 If any of the required router virtual fields are not defined, then the export silently declines.
61
62 =back
63
64 The export itself takes no options.
65
66 =cut
67
68 use strict;
69 use vars qw(@ISA %info $me $DEBUG);
70 use Tie::IxHash;
71 use Text::Template;
72
73 use FS::Record qw(qsearchs);
74 use FS::part_export;
75
76 @ISA = qw(FS::part_export);
77
78 tie my %options, 'Tie::IxHash',
79   'protocol' => {
80           label=>'Protocol',
81           type =>'select',
82           options => [qw(telnet ssh)],
83           default => 'telnet'},
84 ;
85
86 %info = (
87   'svc'     => 'svc_broadband',
88   'desc'    => 'Send a command to a router.',
89   'options' => \%options,
90   'no_machine' => 1,
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.',
92 );
93
94 $me = '[' . __PACKAGE__ . ']';
95 $DEBUG = 1;
96
97
98 sub rebless { shift; }
99
100 sub _field_prefix { 'admin'; }
101
102 sub _req_router_fields {
103   map {
104     $_[0]->_field_prefix . '_' . $_
105   } (qw(address prompt user));
106 }
107
108 sub _export_insert {
109   my($self) = shift;
110   warn "Running insert for " . ref($self);
111   $self->_export_command('insert', @_);
112 }
113
114 sub _export_delete {
115   my($self) = shift;
116   $self->_export_command('delete', @_);
117 }
118
119 sub _export_suspend {
120   my($self) = shift;
121   $self->_export_command('suspend', @_);
122 }
123
124 sub _export_unsuspend {
125   my($self) = shift;
126   $self->_export_command('unsuspend', @_);
127 }
128
129 sub _export_replace {
130   my($self) = shift;
131   $self->_export_command('replace', @_);
132 }
133
134 sub _export_command {
135   my ($self, $action, $svc_broadband) = (shift, shift, shift);
136   my ($error, $old);
137   
138   if ($action eq 'replace') {
139     $old = shift;
140   }
141
142  warn "[debug]$me Processing action '$action'" if $DEBUG;
143
144   # fetch router info
145   my $router = $self->_get_router($svc_broadband, @_);
146   unless ($router) {
147     return "Unable to lookup router for $action export";
148   }
149
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..."
153       if $DEBUG;
154     return '';
155   }
156
157   my $args;
158   ($error, $args) = $self->_prepare_args(
159     $action,
160     $router,
161     $svc_broadband,
162     ($old ? $old : ()),
163     @_
164   );
165
166   if ($error) {
167     # Error occured while preparing args.
168     return $error;
169   } elsif (not defined $args) {
170     # Silently decline.
171     warn "[debug]$me Declining '$action' export" if $DEBUG;
172     return '';
173   } # else ... queue the export.
174
175   warn "[debug]$me Queueing with args: " . join(', ', @$args) if $DEBUG;
176
177   return(
178     $self->_queue(
179       $svc_broadband->svcnum,
180       $self->_get_cmd_sub($svc_broadband, $router),
181       @$args
182     )
183   );
184
185 }
186
187 sub _prepare_args {
188
189   my ($self, $action, $router, $svc_broadband) = (shift, shift, shift, shift);
190   my $old = shift if ($action eq 'replace');
191   my $error = '';
192
193   my $field_prefix = $self->_field_prefix;
194   my $command = $router->getfield("${field_prefix}_cmd_${action}");
195   unless ($command) {
196     warn "[debug]$me router custom field '${field_prefix}_cmd_$action' "
197       . "is not defined." if $DEBUG;
198     return '';
199   }
200
201   if ($command =~ /\[\@--/) { # Use Text::Template
202
203     my $template_data = {};
204
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;
209     } else {
210       $template_data->{$_} = $svc_broadband->getfield($_)
211         foreach $svc_broadband->fields;
212     }
213
214     my $template = new Text::Template (
215       TYPE => 'STRING',
216       SOURCE => $command,
217       DELIMITERS => [ '[@--', '--@]' ],
218     ) or return "Unable to construct template for router command: "
219                 . $Text::Template::ERROR;
220
221     $command = $template->fill_in(
222       HASH => $template_data,
223       BROKEN_ARG => \$error,
224       BROKEN => sub {
225         my %bargs = @_;
226         my $err = $bargs{'arg'};
227         $$err = $bargs{'error'};
228         return undef;
229       },
230     );
231
232     if (not defined $command or $error) {
233       $error ||= $Text::Template::ERROR;
234       return "Unable to fill-in template for router command: $error";
235     }
236
237   } else { # Use eval
238     no strict 'vars';
239     no strict 'refs';
240
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"));
245     } else {
246       ${$_} = $svc_broadband->getfield($_) foreach $svc_broadband->fields;
247       $command = eval(qq("$command"));
248     }
249     return $@ if $@;
250   }
251
252   my $args = [
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,
259   ];
260
261   my $error_check = $router->getfield("${field_prefix}_cmd_${action}_error");
262   push(@$args, ('error_check' => $error_check)) if ($error_check);
263
264   return('', $args);
265
266 }
267
268 sub _get_cmd_sub {
269
270   my ($self, $svc_broadband, $router) = (shift, shift, shift);
271
272   my $protocol = (
273     $router->getfield($self->_field_prefix . '_protocol') =~ /^(telnet|ssh)$/
274   ) ? $1 : 'telnet';
275
276   return(ref($self)."::".$protocol."_cmd");
277
278 }
279
280 sub _check_router_fields {
281
282   my ($self, $router, $action) = (shift, shift, shift);
283   my @check_fields = $self->_req_router_fields;
284
285   foreach (@check_fields) {
286     if ($router->getfield($_) eq '') {
287       warn "[debug]$me Required field '$_' is unset" if $DEBUG;
288       return 0;
289     } else {
290       return 1;
291     }
292   }
293
294 }
295
296 sub _queue {
297   my( $self, $svcnum, $cmd_sub ) = (shift, shift, shift);
298   my $queue = new FS::queue {
299     'svcnum' => $svcnum,
300   };
301   $queue->job($cmd_sub);
302   $queue->insert(@_);
303 }
304
305 sub _get_router {
306   my ($self, $svc_broadband, %args) = (shift, shift, @_);
307
308   my $router;
309   if ($args{'routernum'}) {
310     $router = qsearchs('router', { routernum => $args{'routernum'}});
311   } else {
312     $router = $svc_broadband->router;
313   }
314
315   return($router);
316
317 }
318
319
320 # Subroutines
321 sub ssh_cmd {
322   my %arg = @_;
323
324   eval 'use Net::SSH \'0.08\'';
325   die $@ if $@;
326
327   my @out = &Net::SSH::ssh_cmd( { @_ } );
328   my $error = &_cmd_error_check(\%arg, \@out);
329
330   die ("Error while processing ssh command: $error") if $error;
331
332   return '';
333
334 }
335
336 sub telnet_cmd {
337   my %arg = @_;
338
339   eval 'use Net::Telnet';
340   die $@ if $@;
341
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);
348
349   die ("Error while processing telnet command: $error") if $error;
350
351   return '';
352
353 }
354
355 sub _cmd_error_check {
356   my ($arg, $out) = (shift, shift);
357
358   die "_cmd_error_check called without proper arguments"
359     unless (ref($arg) eq 'HASH' and ref($out) eq 'ARRAY');
360
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;
364     return '';
365   }
366
367   my $error_check = $arg->{'error_check'};
368   foreach (@$out) {
369     return $_ if /$error_check/;
370   }
371
372   return '';
373
374 }
375
376 1;